<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>编程手写题练习</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.7.0/build/highlight.min.js"></script>
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.7.0/build/styles/atom-one-dark.min.css">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#36D399',
            accent: '#FF9F43',
            dark: '#1E293B',
            light: '#F8FAFC'
          },
          fontFamily: {
            inter: ['Inter', 'system-ui', 'sans-serif'],
          },
        },
      }
    }
  </script>
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .text-shadow {
        text-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .card-hover {
        transition: all 0.3s ease;
      }
      .card-hover:hover {
        transform: translateY(-5px);
        box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
      }
      .fixed-action-btn {
        position: fixed;
        bottom: 2rem;
        right: 2rem;
        z-index: 100;
        opacity: 0;
        transition: opacity 0.3s ease;
      }
      .fixed-action-btn.visible {
        opacity: 1;
      }
    }
  </style>
</head>

<body class="font-inter bg-gray-50 text-dark min-h-screen flex flex-col">
  <!-- 导航栏 -->
  <header class="bg-white shadow-md sticky top-0 z-50 transition-all duration-300" id="navbar">
    <div class="container mx-auto px-4 py-3 flex justify-between items-center">
      <div class="flex items-center space-x-2">
        <i class="fa fa-code text-primary text-2xl"></i>
        <h1 class="text-xl font-bold text-primary">编程手写题练习</h1>
      </div>
      <div class="hidden md:flex items-center space-x-6" id="desktop-nav">
        <!-- 桌面端导航栏将通过JavaScript动态生成 -->
      </div>
      <button id="mobile-menu-btn" class="md:hidden text-gray-600 focus:outline-none">
        <i class="fa fa-bars text-xl"></i>
      </button>
    </div>
    <!-- 移动端菜单 -->
    <div id="mobile-menu" class="md:hidden hidden bg-white border-t border-gray-100">
      <div class="container mx-auto px-4 py-2 flex flex-col space-y-2" id="mobile-nav">
        <!-- 移动端导航栏将通过JavaScript动态生成 -->
      </div>
    </div>
  </header>

  <!-- 主内容区 -->
  <main class="flex-grow container mx-auto px-4 py-8">
    <div class="max-w-4xl mx-auto">
      <!-- 随机出题按钮 -->
      <div class="flex justify-center mb-8">
        <button id="random-btn"
          class="bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg font-medium shadow-lg shadow-primary/20 flex items-center space-x-2 transition-all duration-300 transform hover:scale-105">
          <i class="fa fa-random"></i>
          <span>随机出题</span>
        </button>
      </div>

      <!-- 题目卡片 -->
      <div id="question-card"
        class="bg-white rounded-2xl shadow-xl overflow-hidden mb-8 transform transition-all duration-500 opacity-0 scale-95">
        <div class="p-6 border-b border-gray-100">
          <div class="flex justify-between items-start mb-4">
            <span id="question-tag"
              class="px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary"></span>
            <span id="question-count" class="text-xs text-gray-500"></span>
          </div>
          <h2 id="question-title" class="text-2xl font-bold mb-4 text-dark"></h2>
          <div id="question-content" class="text-gray-700 mb-6 whitespace-pre-wrap"></div>
        </div>

        <!-- 答案区域 -->
        <div id="answer-container" class="p-6 hidden">
          <h3 class="text-xl font-semibold mb-4 flex items-center">
            <i class="fa fa-lightbulb-o text-yellow-500 mr-2"></i>
            参考答案
          </h3>
          <div id="answers" class="space-y-6"></div>
        </div>

        <!-- 查看/隐藏答案按钮 -->
        <div class="px-6 pb-6" id="answer-buttons-container">
          <button id="show-answer-btn"
            class="w-full bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 rounded-lg font-medium transition-all duration-300">
            <i class="fa fa-eye mr-2"></i>
            查看参考答案
          </button>
        </div>
      </div>

      <!-- 固定位置的隐藏答案按钮 -->
      <button id="fixed-hide-answer-btn"
        class="fixed-action-btn bg-primary hover:bg-primary/90 text-white p-3 rounded-full shadow-lg shadow-primary/20 transition-all duration-300 transform hover:scale-105">
        <i class="fa fa-eye-slash"></i>
      </button>

      <!-- 题目列表 -->
      <div id="questions-list" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
        <!-- 题目卡片将通过JavaScript动态生成 -->
      </div>
    </div>
  </main>

  <!-- 页脚 -->
  <footer class="bg-dark text-white py-8">
    <div class="container mx-auto px-4">
      <div class="flex flex-col md:flex-row justify-between items-center">
        <div class="mb-4 md:mb-0">
          <div class="flex items-center space-x-2">
            <i class="fa fa-code text-primary text-xl"></i>
            <span class="font-bold text-lg">编程手写题练习</span>
          </div>
          <p class="text-gray-400 text-sm mt-2">提升你的编程能力，掌握核心概念</p>
        </div>
        <div class="flex space-x-6">
          <a href="#" class="text-gray-400 hover:text-white transition-colors">
            <i class="fa fa-github text-xl"></i>
          </a>
          <a href="#" class="text-gray-400 hover:text-white transition-colors">
            <i class="fa fa-twitter text-xl"></i>
          </a>
          <a href="#" class="text-gray-400 hover:text-white transition-colors">
            <i class="fa fa-linkedin text-xl"></i>
          </a>
        </div>
      </div>
      <div class="border-t border-gray-700 mt-6 pt-6 text-center text-gray-400 text-sm">
        &copy; 2025 编程手写题练习 | 为程序员打造的技术提升平台
      </div>
    </div>
  </footer>

  <!-- 代码高亮样式 -->
  <style>
    .code-block {
      position: relative;
      margin-top: 1rem;
      border-radius: 0.5rem;
      overflow: hidden;
      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    }

    .code-header {
      background-color: #2d2d2d;
      color: #cccccc;
      padding: 0.5rem 1rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 0.875rem;
    }

    .code-body {
      background-color: #282c34;
      color: #abb2bf;
      padding: 1rem;
      overflow-x: auto;
      font-family: Consolas, Monaco, 'Andale Mono', monospace;
      font-size: 0.875rem;
      line-height: 1.5;
    }

    .copy-btn {
      background-color: rgba(255, 255, 255, 0.1);
      color: #cccccc;
      padding: 0.25rem 0.5rem;
      border-radius: 0.25rem;
      cursor: pointer;
      font-size: 0.75rem;
      transition: background-color 0.2s;
    }

    .copy-btn:hover {
      background-color: rgba(255, 255, 255, 0.2);
    }

    .copy-btn.copied {
      background-color: #4CAF50;
    }
  </style>

  <script>
    // 总的问题数据
    const total_questions = [
      {
        tag: `js 基础`,
        data: [
          {
            question: "手写 new 操作符",
            description: `手写 new 操作符`,
            answer: [
              {
                text: '',
                daima: `
function mockNew(constructor, ...args) {
  // 1.创建一个新对象 obj
  const obj = {};
  // 2.把构造函数当参数传入，新对象指向构造函数原型对象
  obj.__proto__ = constructor.prototype;
  // 3.通过 apply 将构建函数的 this 指向新对象
  let result = constructor.apply(obj, args);
  // 4.根据返回值判断
  return result instanceof Object ? result : obj;
}
`
              }
            ]
          },
          {
            question: "手写 call 方法",
            description: `手写 call 方法`,
            answer: [
              {
                text: "Function.prototype.call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。",
                daima: `
Function.prototype.myCall = function(context = window, ...args) {
  // this 指向调用 myCall 的函数
  const fn = Symbol('fn');
  context[fn] = this;
  
  const result = context[fn](...args);
  delete context[fn];
  return result;
}
`
              }
            ]
          },
          {
            question: "手写 apply 方法",
            description: `手写 apply 方法`,
            answer: [
              {
                text: "Function.prototype.apply() 方法调用一个具有给定 this 值的函数，以及以一个数组（或类数组对象）的形式提供的参数。",
                daima: `
Function.prototype.myApply = function(context = window, args = []) {
  // this 指向调用 myApply 的函数
  const fn = Symbol('fn');
  context[fn] = this;
  
  const result = context[fn](...args);
  delete context[fn];
  return result;
}
`
              }
            ]
          },
          {
            question: `2、手写 Object.create`,
            description: `2、手写 Object.create`,
            answer: [
              {
                text: ``,
                daima: `// Object.create 用于创建新对象,使用现有的对象来作为新创建对象的原型。
// 通过 object.create 创建的对象不会继承 Object.prototype 上的属性和方法
function mockCreate(obj) {
    function F() {};    // 创建一个新对象
    F.prototype = obj;  // 将该函数的原型设置为指定的对象
    return new F();     // 返回一个新对象
}`
              }
            ]
          },
          {
            question: `3、手写 instanceof `,
            description: `3、手写 instanceof `,
            answer: [
              {
                text: ``,
                daima: `// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
//如果检测的类型在当前实例的原型链上，则返回 true，说明这个实例属于这个类型，否则返回 false。
function mockInstanceof(left, right) {
    let proto = Object.getPrototypeOf(left);      // 获取对象的原型
    let prototype = right.prototype;              // 获取构造函数的 prototype 对象
    // 判断构造函数的 prototype 对象是否在对象的原型链上
    while(proto !== null) {         
        if (proto === prototype) return true;
        proto= Object.getPrototypeOf(proto); // 继续查找原型链
    }
    return false;
}`
              }
            ]
          },
          {
            question: `4、手写类型判函数`,
            description: `4、手写类型判函数`,
            answer: [
              {
                text: ``,
                daima: `function mockType(value) {
  // 判断数据是 null 的情况
  if (value === null) {
    return value + "";
  }
  // 判断数据是引用类型的情况
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value);
    let type = valueClass.split(" ")[1].split("");
    type.pop();
    return type.join("").toLowerCase();
  } else {
    // 判断数据是基本数据类型的情况和函数的情况
    return typeof value;
  }
}`
              }
            ]
          },
          {
            question: `5、手写节流函数`,
            description: `5、手写节流函数`,
            answer: [
              {
                text: `时间搓写法(立即执行)`,
                daima: `function throttle(fn, millisecond) {
    let start = 0;             //先把开始时间定义为0 (第一次立刻执行)
    return function () {
        let end = Date.now();  //结束时间直接获取当前
        if ((end - start) >= millisecond) { //结束时间-开始时间>=时间参数 (执行)
            start = end;       //结束时间赋值给开始时间，为一下次执行做准备
            fn.apply(this, arguments); //执行函数，注意要用 apply 来改变 this 指向
        }
    }
}`
              },
              {
                text: `定时器写法`,
                daima: `function throttle(fn, time) {
    let isRun = false;
    return function (...args) {
        if (isRun) {
            return;
        }
        isRun = true;
        setTimeout(() => {
            fn.apply(this, args);
            isRun = false;
        }, time)
    }
}`
              }
            ]
          },
          {
            question: `6、手写防抖函数`,
            description: `6、手写防抖函数`,
            answer: [
              {
                text: ``,
                daima: `function debounce(fn, time) {
    let timer = null;
    return function (...args) {
        if (timer) {  // 如果存在定时器，则取消之前的定时器重新记时
            clearTimeout(timer);
            timer = null;
        }
        timer = setTimeout(() => {  // 设置定时器
            fn.apply(this, args);
        }, time);
    }
}`
              }
            ]
          },
          {
            question: `7、实现深拷贝`,
            description: `7、实现深拷贝`,
            answer: [
              {
                text: `方法一：递归实现`,
                daima: `function deepCopy(obj) {
    let res = Array.isArray(obj) ? [] : {};
    for (let i in obj) {
        if (obj.hasOwnProperty(i)) {
            res[i] = typeof obj[i] === "object" ? deepCopy(obj[i]) : obj[i];
        }
    }
    return res;
}`
              },
              {
                text: `方法二：JSON.parse(JSON.stringify()) 序列化`,
                daima: `// 利用JSON.stringify将js对象序列化成JSON字符串，再使用JSON.parse来反序列化还原js对象。
let obj1 = { 
    a: 0,
    b: {c: 0}
};`
              },
              {
                text: `方法三：扩展运算符...`,
                daima: `let a = {
    name: '张三',
    age: 18
}
let b = { ...a };`
              }
            ]
          },
          {
            question: `8、手写浅拷贝`,
            description: `8、手写浅拷贝`,
            answer: [
              {
                text: ``,
                daima: `function shallowCopy(object) {
    //只拷贝对象
    if (!object || typeof object !== "object") return;
    //根据 object 的类型判断是新建一个数组还是对象
    let newObject = Array.isArray(object) ? [] : {};
    //遍历 object，并且判断是 object 的属性才拷贝
    for (let key in object) {
        if (object.hasOwnProperty(key)) {
            newObject[key] = object[key];
        }
    }
    return newObject;
}
let obj1 = {a:1,b:{c:1}}
var obj2 = shallowCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1);  // {a:2,b:{c:3}}
console.log(obj2);  // {a:1,b:{c:3}}`
              },
              {
                text: `Object.assign 实现浅拷贝`,
                daima: `// Object.assign 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
let target = { a: 1 }
let obj1 = { b: 2 }
let obj2 = { c: 3 }
Object.assign(target, obj1, obj2);
console.log(target); // {a:1,b:2,c:3}`
              }
            ]
          },
          {
            question: `9、手写 call 函数`,
            description: `9、手写 call 函数`,
            answer: [
              {
                text: ``,
                daima: `// 原生JS提供了call、apply、bind三种方式来修改this指向。
// call、apply会立即执行，bind返回一个新函数。
// call、apply临时改变this指向一次，bind永久改变this指向。
// apply第二个参数是数组，call和bing没有限制参数类型。
 
// 将myCall方法绑定到function原型链上。
Function.prototype.myCall = function(context, ...args) { 
  context = context || window;   // context就是this要指向的对象，不设置默认window
  args = args ? args : [];        // args不传时默认是空数组,防止下面用spread操作符时报错
  let fn = Symbol();             // 生成一个唯一值
  context[fn] = this;            // 把要执行的函数绑定到context的属性上
  let result = context[fn](...args);  // 调用的函数，this指向context
  delete context[fn];
  return result;
}`,
              }
            ]
          },
          {
            question: `10、手写 apply 函数`,
            description: `10、手写 apply 函数`,
            answer: [
              {
                text: ``,
                daima: `Function.prototype.myApply = function(context, args) {
  context = context || window; 
  args = args ? args : [];
  const fn = Symbol();
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
}`,
              }
            ]
          },
          {
            question: `11、手写 bind 函数`,
            description: `11、手写 bind 函数`,
            answer: [
              {
                text: ``,
                daima: `Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') { //必须是函数才能调用bind
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let fn = this;                        //先获取要执行的函数
    let agrs = [...arguments].slice(1);   //调用myBind时传入的参数
    let fn2 = function(){}                //创建空函数来中转
    let bindFn = function () {
        let agrs2 = [...arguments];       //调用bindFn时传入的参数
        let Allagrs = agrs.concat(agrs2); //合并两组参数
        //判断是new调用还是普通调用，且使用apply修改this的指向
        fn.apply(this instanceof fn ? this : context, Allagrs);
    }
    fn2.prototype = fn.prototype;      
    bindFn.prototype = new fn2();         //原型链继承
    return bindFn;
}
 
let obj = {
    name: 'xiaomin'
};
function show(age, sex) {
    this.name = this.name;
    this.age = age;
    this.sex = sex;
    this.height = 176;
}
show.prototype.hobby = '打王者';
 
let bindFn = show.myBind(obj, 18);
let boy = new bindFn('男');
console.log(boy);  //{name: undefined, age: 18, sex: "男", height: 176}
console.log(boy.hobby)   //打王者
console.log(bindFn.prototype.constructor === show); //true`,
              }
            ]
          },
          {
            question: `12、实现函数柯里化`,
            description: `12、实现函数柯里化`,
            answer: [
              {
                text: ``,
                daima: `// 将接受多个参数的函数变换成接收一个单一参数的函数，并返回一个接收剩余参数的新函数。
// 作用：参数复用、提前返回和延迟执行。
function myCurry(fn) {
  // 获取函数需要的参数长度
  var length = fn.length;   
  var args = args || [];
  return function() {
    var newArgs = args.concat(Array.prototype.slice.call(arguments))
    // 判断参数的长度是否已经满足函数所需参数的长度
    if(newArgs.length < length) {
      // 如果不满足，递归返回科里化的函数，等待参数的传入
      return myCurry.call(this, fn, newArgs);
    } else {
      // 如果满足，执行函数
      return fn.apply(this, newArgs);
    }
  }
}`,
              },
              {
                text: `es6 实现函数柯里化`,
                daima: `function myCurry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}`
              }
            ]
          },
          {
            question: `13、实现 Promise`,
            description: `13、实现 Promise`,
            answer: [
              {
                text: ``,
                daima: `const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class Promise {
  constructor(executor) {
    this.status = PENDING; // 默认状态为 PENGING
    this.value = undefined; // 存放成功状态得值
    this.reason = undefined; // 存放失败状态得值
    this.handleFulfilled = []; // 存储成功后的回调
    this.handleRejection = []; // 存储失败后的回调
    // ! resolve 形参的实际参数在这儿
    const resolve = (data) => {
      // 状态变更只有一次
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = data;
        this.handleFulfilled.forEach((fn) => fn(data));
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.handleRejection.forEach((fn) => fn(reason));
      }
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      // 遇到错误时，捕获错误，执行 reject 函数
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      this.handleFulfilled.push(() => onFulfilled(this.value));
      this.handleRejection.push(() => onRejected(this.reason));
    }
    return this;
  }
}`,
              }
            ]
          },
          {
            question: `14、实现 Promise.all, Promise.race, Promise.any, Promise.resolve, Promise.reject`,
            description: `14、实现 Promise.all, Promise.race, Promise.any, Promise.resolve, Promise.reject`,
            answer: [
              {
                text: `promise.catch()`,
                daima: `Promise.prototype.catch = function (errCallback) {
  return this.then(undefined, errCallback);
};`,
              },
              {
                text: `promise.filally()`,
                daima: `romise.prototype.finally = function (callback) {
  return this.then(
    (value) => {
      return Promise.resolve(callback(value)).then(() => value);
    },
    (reason) => {
      return Promise.resolve(callback(reason)).then(() => {
        throw reason;
      });
    }
  );
};`,
              },
              {
                text: `promise.resolve()`,
                daima: `默认产生一个成功的 promise。Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 promise ，那么将返回这个 promise ；如果这个值是 thenable（即带有"then" 方法），返回的 promise 会“跟随”这个 thenable 的对象，采用它的最终状态；否则返回的 promise 将以此值完成。此函数将类 promise 对象的多层嵌套展平。
                static resolve(data){
  return new Promise((resolve,reject)=>{
    resolve(data);
  })
}
如果参数是 promise 会等待这个 promise 解析完毕，在向下执行，所以这里需要在 constructor 方法中做一个小小的处理：
let reject = (reason) => {
  if (this.status === PENDING) {
    this.status = REJECTED;
    this.reason = reason;
    this.onRejectedCallbacks.forEach((fn) => fn());
  }
};

let resolve = (value) => {
  // 如果 value 是一个promise，那我们的库中应该也要实现一个递归解析
  if (value instanceof Promise) {
    // 递归解析
    return value.then(resolve, reject);
  }
  if (this.status === PENDING) {
    this.status = FULFILLED;
    this.value = value;
    this.onResolvedCallbacks.forEach((fn) => fn());
  }
};`,
              },
              {
                text: `promise.reject()`,
                daima: `默认产生一个失败的 promise，Promise.reject 是直接将值变成错误结果。
                static reject(reason){
  return new Promise((resolve,reject)=>{
    reject(reason);
  })
}`
              },
              {
                text: `promise.all()`,
                daima: `promise.all 是解决并发问题的，多个异步并发获取最终的结果（如果有一个失败则失败）。
                Promise.all = function (values) {
  if (!Array.isArray(values)) {
    const type = typeof values;
    return new TypeError('TypeError: ￥{type} ￥{values} is not iterable');
  }
  return new Promise((resolve, reject) => {
    let resultArr = [];
    let orderIndex = 0;
    const processResultByKey = (value, index) => {
      resultArr[index] = value;
      if (++orderIndex === values.length) {
        resolve(resultArr);
      }
    };
    for (let i = 0; i < values.length; i++) {
      let value = values[i];
      if (value && typeof value.then === "function") {
        value.then((value) => {
          processResultByKey(value, i);
        }, reject);
      } else {
        processResultByKey(value, i);
      }
    }
  });
};`,
              },
              {
                text: `promise.race()`,
                daima: `Promise.race 用来处理多个请求，采用最快的（谁先完成用谁的）。
                Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    // 一起执行就是for循环
    for (let i = 0; i < promises.length; i++) {
      let val = promises[i];
      if (val && typeof val.then === "function") {
        val.then(resolve, reject);
      } else {
        // 普通值
        resolve(val);
      }
    }
  });
};
这里第一个 promise 最快，所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后，所有进一步的 result/error 都会被忽略。

例如，这里的结果将是 1：
Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Whoops!")), 2000)
  ),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(alert); // 1`,
              },
              {
                text: `promise.any()`,
                daima: `与 Promise.race 类似，区别在于 Promise.any 只等待第一个 fulfilled 的 promise，并将这个 fulfilled 的 promise 返回，它不会等待其他的 promise 全部完成。如果给出的 promise 都 rejected，那么则返回 rejected 的 promise 和 AggregateError 错误类型的 error 实例—— 一个特殊的 error 对象，在其 errors 属性中存储着所有 promise error。

例如，这里的结果将是 1：
Promise.any([
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Whoops!")), 1000)
  ),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
这里的第一个 promise 是最快的，但 rejected 了，所以第二个 promise 则成为了结果。在第一个 fulfilled 的 promise “赢得比赛”后，所有进一步的结果都将被忽略。

这是一个所有 promise 都失败的例子：]).then(alert); // 1
Promise.any([
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Ouch!")), 1000)
  ),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error!")), 2000)
  ),
]).catch((error) => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error!
});
正如你所看到的，我们在 AggregateError 错误类型的 error 实例的 errors 属性中可以访问到失败的 promise 的 error 对象。

实现该方法，可以通过上面的两个例子：

Promise.any 只要传入的 promise 有一个是 fullfilled 则立即 resolve 出去，否则将所有 reject 结果收集起来并返回 AggregateError
Promise.any = function (promises) {
  return new Promise((resolve, reject) => {
    promises = Array.isArray(promises) ? promises : [];
    let len = promises.length;
    // 用于收集所有 reject
    let errs = [];
    // 如果传入的是一个空数组，那么就直接返回 AggregateError
    if (len === 0)
      return reject(new AggregateError("All promises were rejected"));
    promises.forEach((promise) => {
      promise.then(
        (value) => {
          resolve(value);
        },
        (err) => {
          len--;
          errs.push(err);
          if (len === 0) {
            reject(new AggregateError(errs));
          }
        }
      );
    });
  });
};`,
              },
              {
                text: `promise.allSettled()`,
                daima: `如果任意的 promise reject，则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时，它对这种“全有或全无”的情况很有用：Promise.allSettled 等待所有的 promise 都被 settle，无论结果如何。结果数组具有：

{status:"fulfilled", value:result} 对于成功的响应，
{status:"rejected", reason:error} 对于 error。
Promise.allSettled = function (promises) {
  const rejectHandler = (reason) => ({ status: "rejected", reason });
  const resolveHandler = (value) => ({ status: "fulfilled", value });
  const convertedPromises = promises.map((p) =>
    Promise.resolve(p).then(resolveHandler, rejectHandler)
  );
  return Promise.all(convertedPromises);
};`,
              }
            ]
          },
          {
            question: `15、实现 AXAJ 请求`,
            description: `15、实现 AXAJ 请求`,
            answer: [
              {
                text: `这个可以说自己不会写但是自己是知道一些原理和其他的东西的，比如：浏览器专用的fetch，浏览器和node通用的axios，axios就是通过原生ajax封装的一个库，所以是浏览器和node通用的，然后在node也有node专用的发送请求的方式，比如：http/https模块`,
                daima: `然后除了这个东西还有可以引申一点其他的方面的东西，比如：websockets实时通信方法，还有sse这种的方式。
                但是在一般的项目中是自己封装的一套请求方式用来向服务端发送请求，这样会方便管理`,
              }
            ]
          },
          {
            question: `16、使用 Promise 封装 AJAX 函数`,
            description: `16、使用 Promise 封装 AJAX 函数`,
            answer: [
              {
                text: `如果遇到了这个问题可以直接跳过，告诉面试官自己不会`,
                daima: `如果真的遇到了这个问题，到时候可以告诉面试官自己由于不太了解这个 ajax 的函数和 api 但是自己对promise有一定的了解，能不能多问问关于promise的问题`,
              }
            ]
          },
          {
            question: `1. 说说JavaScript中的数据类型？存储上的差别?`,
            description: ``,
            answer: [
              {
                text: `JavaScript 数据类型分为 基本类型 和 引用类型，它们在存储方式、内存管理和使用方式上有本质区别。`,
                daima: `1. 基本数据类型（Primitive Types）
分类：undefined、null、boolean、number、string、symbol（ES6）、bigint（ES2020）。
特点：
值不可变：修改值会创建新值，而非改变原值。
直接存储：变量直接存储在 栈内存（Stack） 中，访问速度快。
按值传递：赋值或传参时复制值本身。
2. 引用数据类型（Reference Types）
分类：object（包括数组、函数、日期等）。
特点：
值可变：可修改对象内部属性。
间接存储：
栈内存：存储变量名和引用地址（指向堆内存中的实际对象）。
堆内存（Heap）：存储对象的实际内容（无序、动态分配）。
按引用传递：赋值或传参时复制引用地址，而非对象本身。`
              }
            ]
          },
          {
            question: `1. 如何判断数据类型？`,
            description: ``,
            answer: [
              {
                text: `基础方法：typeof、instanceof、Object.prototype.toString.call()。`,
                daima: `typeof null; // 'object'（历史遗留问题）
[] instanceof Array; // true
Object.prototype.toString.call(new Date()); // '[object Date]'`
              }
            ]
          },
          {
            question: `2. 说说你了解的js数据结构？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `下面是常见的数据结构：
1. 数组 (Array)
2. 栈 (Stack)
3. 队列 (Queue)
4. 链表 (Linked List)
5. 字典
6. 散列表 (Hash table)
7. 树 (Tree)
8. 图 (Graph)
9. 堆 (Heap)`
              }
            ]
          },
          {
            question: `3.DOM常见的操作有哪些？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `1. 节点的选择与查询
                2. 节点创建与插入
                3. 节点修改与替换
                4. 节点删除
                5. 节点遍历与关系
                  这个可以获取到父节点和兄弟节点
                6. DOM 属性与特性（Attributes vs Properties）
                7. DOM 事件操作
                  事件监听
                  事件移除
                  事件委托
                8. DOM 性能优化
                9. DOM 节点信息获取
                10. 高级操作
                  克隆节点：
                  创建 DocumentFragment：
                  自定义事件：
                `
              },
              {
                text: `其他的面试高频问题`,
                daima: `1. innerHTML和textContent的区别
                innerHTML 解析 HTML，可能导致 XSS 风险；textContent 仅设置纯文本。
                2.事件冒泡和事件捕获
                冒泡：事件从触发元素向上传播到根节点。
捕获：事件从根节点向下传播到触发元素（默认 addEventListener 为冒泡阶段）。
                3. 如何优化大量DOM操作
                使用 DocumentFragment 批量操作。
缓存 DOM 查询结果。
使用 requestAnimationFrame 优化动画。
                4. 什么是重排和重绘？如何避免？
                重排：DOM 变化导致布局重新计算（如修改宽高）。
重绘：元素外观变化但不影响布局（如颜色修改）。
避免频繁读写 DOM，使用 classList 批量修改样式。`,
              }
            ]
          },
          {
            question: `4.说说你对BOM的理解，常见的BOM对象你了解哪些？`,
            description: ``,
            answer: [
              {
                text: `BOM（Browser Object Model，浏览器对象模型）是 浏览器提供的一套用于操作浏览器窗口和浏览器环境的 API，它以 window 对象为核心，将浏览器的各个部分抽象为对象，允许 JavaScript 与浏览器进行交互（如控制窗口、操作地址栏、管理历史记录等）。
                与 DOM（操作文档内容）不同，BOM 没有统一的标准（由浏览器厂商各自实现，存在一定差异），但核心功能在各浏览器中基本一致。`,
                daima: `常见的 BOM 对象
                1. window对象
                地位：BOM 的顶层对象，所有全局变量、函数、BOM/DOM 对象都是 window 的属性。
                2. location 对象
                作用：管理当前页面的 URL 信息，可用于获取或修改 URL、跳转页面。
                核心属性和方法：
属性：
location.href：完整 URL（如 https://example.com/path?name=test#hash），修改该属性会跳转页面。
location.protocol：协议（如 http: 或 https:）。
location.host：主机名 + 端口（如 example.com:8080）。
location.pathname：路径（如 /path）。
location.search：查询参数（如 ?name=test）。
location.hash：哈希值（如 #hash，常用于单页应用路由）。
方法：
location.assign(url)：跳转至指定 URL（保留历史记录，可回退）。
location.replace(url)：跳转至指定 URL（替换当前历史记录，不可回退）。
location.reload()：刷新当前页面（true 表示强制从服务器刷新）。
              3. history 对象
              作用：管理浏览器的历史记录（即用户浏览过的页面），可用于前进 / 后退页面。
              核心方法：
history.back()：后退到上一页（等同于浏览器的 “后退” 按钮）。
history.forward()：前进到下一页（等同于浏览器的 “前进” 按钮）。
history.go(n)：跳转到历史记录中第 n 页（n=1 前进，n=-1 后退，n=0 刷新）。
history.pushState(state, title, url)：添加新历史记录（不刷新页面，常用于单页应用路由）。
history.replaceState(state, title, url)：替换当前历史记录（不刷新页面）。
                4. navigator 对象
                作用：提供浏览器自身的信息（如浏览器类型、版本、操作系统等），常用于浏览器检测。
常用属性：
navigator.userAgent：浏览器的用户代理字符串（包含浏览器类型、版本、操作系统等信息，常用于判断设备或浏览器）。
navigator.platform：用户的操作系统平台（如 Win32、MacIntel）。
navigator.language：浏览器的默认语言（如 zh-CN）。
navigator.onLine：布尔值，表示浏览器是否联网（true 为在线）。
5. screen 对象
作用：提供用户屏幕的信息（如屏幕尺寸、分辨率等），常用于适配不同屏幕。
常用属性：
screen.width / screen.height：屏幕的总宽度 / 高度（像素）。
screen.availWidth / screen.availHeight：屏幕可用宽度 / 高度（扣除任务栏等系统界面后的尺寸）。
screen.colorDepth：屏幕的颜色深度（如 24 位表示支持 1600 万色）。
6. document 对象
说明：严格来说 document 属于 DOM，但由于它是 window 的属性（window.document），且常与 BOM 一起讨论，因此也被视为 BOM 相关对象。
作用：操作 HTML 文档内容（如获取元素、修改结构等），是 DOM 的核心对象。
                `
              },
              {
                text: `相关的其他的面试考点`,
                daima: `1. BOM 与 DOM 的区别：
BOM 操作浏览器窗口和环境（如 window、location），无统一标准；
DOM 操作 HTML 文档内容（如 document），有 W3C 标准。
2. location.href 与 location.replace() 的区别：
location.href = url 会添加新历史记录，可回退；
location.replace(url) 替换当前历史记录，不可回退。
3. 如何实现单页应用（SPA）的路由跳转：
利用 history.pushState() 或 location.hash 实现无刷新跳转，结合 popstate 事件监听路由变化。
4. userAgent 的作用及局限性：
用于判断浏览器或设备（如区分移动端 / PC 端），但可能被篡改，可靠性较低。`,
              }
            ]
          },
          {
            question: `5.==和===区别，分别在什么情况使用`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，==（宽松相等）和 ===（严格相等）是两种不同的比较运算符，其核心区别在于是否进行类型转换。
                == 双等号：宽松相等：先进行类型转换（如果类型不同），再比较值。 1 == '1' → true（字符串转为数字）
                === 三等号：严格相等：不进行类型转换，直接比较类型和值。 1 === '1' → false（类型不同）
                现在的开发中绝大多数的使用场景都是使用===三等号，只有在特定的情况下使用==双等号
                判断 null 或 undefined：x == null 等价于 x === null || x === undefined。
                与 null/undefined 宽松比较：当需要同时处理 null 和 undefined 时。
                // 检查配置是否存在（忽略具体是 null 还是 undefined）
if (config.option == null) { /* ... */ }
                `
              },
              {
                text: `其他的面试高频的问题`,
                daima: `1. 为什么推荐使用 ===？
避免隐式类型转换的副作用，减少 bug，提高代码可靠性。
                  2. NaN 的比较特性：
NaN 与任何值（包括自身）比较都返回 false，必须用 isNaN() 或 Object.is() 判断。
                NaN === NaN; // false
isNaN(NaN); // true
Object.is(NaN, NaN); // true
                  3. Object.is() 与 === 的区别：
                  Object.is() 是更严格的相等判断，能正确处理 NaN 和 ±0。
                  Object.is(NaN, NaN); // true
Object.is(0, -0); // false
0 === -0; // true
                  `,
              }
            ]
          },
          {
            question: `6. typeof 与 instanceof 区别`,
            description: ``,
            answer: [
              {
                text: `在 JavaScript 中，typeof 和 instanceof 是两种用于类型判断的操作符，但它们的实现机制和应用场景截然不同。`,
                daima: `1. 核心区别
                typeof	基本类型和对象类型的初步判断	字符串（如 'number'、'object'）	快速区分基本类型（但对 null 和对象区分有限）
                instanceof	对象的原型链	布尔值（true 或 false）	判断对象是否由某个构造函数 / 类创建，或是否继承自某个原型`
              },
              {
                text: `其他的相关的高频面试题`,
                daima: `
                1. 为什么 typeof null === 'object'？
历史遗留 bug：JavaScript 早期版本中，值以二进制存储，null 的二进制全为 0，被误判为对象。
2. 如何区分 null 和对象？
使用 value === null 或 Object.prototype.toString.call(value)。
3. instanceof 在跨窗口 /iframe 中的问题
不同窗口的 Array、Object 构造函数不同，导致 instanceof 判断失效。
解决方案：使用 Array.isArray()（仅对数组有效）或 Object.prototype.toString.call()。
4. typeof 和 instanceof 的适用场景
typeof：快速判断基本类型。
instanceof：判断对象的继承关系。`,
              }
            ]
          },
          {
            question: `7.JavaScript原型，原型链？有什么特点?`,
            description: ``,
            answer: [
              {
                text: `JavaScript 原型与原型链解析`,
                daima: `JavaScript 是一种基于原型（Prototype-based）的语言，它通过原型链（Prototype Chain）实现继承。理解原型与原型链是掌握 JS 核心机制的关键，也是面试中的高频考点。
1. 原型（Prototype）的基本概念
每个对象都有一个内部属性 [[Prototype]]（在浏览器中通常暴露为 __proto__），它指向该对象的原型对象。
原型对象也是普通对象，同样有自己的原型，以此类推，直到最顶层的原型对象 Object.prototype（其 [[Prototype]] 为 null）。
原型的作用：实现属性和方法的共享。当访问一个对象的属性时，JS 会先在该对象本身查找，若未找到，则沿原型链向上查找。
2. 原型链（Prototype Chain）的工作原理
属性查找机制：
当访问 obj.prop 时，JS 会：
先检查 obj 本身是否有 prop 属性；
若没有，则查找 obj.__proto__（即 obj 的原型对象）；
若仍未找到，则继续查找 obj.__proto__.__proto__，以此类推，直到 Object.prototype 或 null。
示例：
const arr = [];
arr.push(1); // 数组本身没有 'push' 方法，从 'Array.prototype' 继承
arr.toString(); // 从 'Object.prototype' 继承
3. 原型链的特点
1. 动态继承：
原型链在运行时动态解析，若修改原型对象，所有继承自它的对象都会受影响。
const obj = {};
Object.prototype.foo = 'bar';
console.log(obj.foo); // 'bar'（动态添加到原型链）
2. 共享属性与方法：
多个对象可通过原型链共享同一组属性和方法，节省内存。
function Person() {}
Person.prototype.sayHello = () => console.log('Hello');

const p1 = new Person();
const p2 = new Person();
p1.sayHello(); // 共享同一方法
3. 顶层原型为 Object.prototype：
所有对象最终继承自 Object.prototype（除了 Object.create(null) 创建的对象）。
const obj = {};
obj.hasOwnProperty('key'); // 从 'Object.prototype' 继承
4. null 终止原型链：
Object.prototype.__proto__ === null，它是原型链的终点。
4. 原型链的创建方式
1. 构造函数：
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = () => console.log('￥{this.name} makes a sound');

const dog = new Animal('Dog');
dog.speak(); // 继承自 Animal.prototype
2. Object.create()：
const parent = { foo: 'bar' };
const child = Object.create(parent);
console.log(child.foo); // 'bar'（继承自 parent）
3. ES6 类（语法糖）：
class Animal {
  speak() { console.log('Animal sound'); }
}
class Dog extends Animal {
  bark() { console.log('Woof'); }
}

const dog = new Dog();
dog.speak(); // 继承自 Animal.prototype
5. 原型链的优缺点
优点：
动态性：运行时可修改原型，灵活扩展功能。
内存高效：共享方法，减少重复创建。
简单灵活：无需复杂的类体系，适合快速迭代。
缺点：
性能问题：多层级的原型链会降低属性查找速度。
共享状态风险：若原型中包含引用类型属性（如数组），所有实例可能共享该状态，导致意外修改。
function Person() {}
Person.prototype.hobbies = [];

const p1 = new Person();
const p2 = new Person();
p1.hobbies.push('reading');
console.log(p2.hobbies); // ['reading']（共享同一数组）
`
              },
              {
                text: `6. 面试高频问题`,
                daima: `1. 如何判断一个属性是对象自身的还是继承的？
                obj.hasOwnProperty('prop'); // true：自身属性，false：继承或不存在
                2. instanceof 的工作原理：
判断对象的原型链中是否存在构造函数的 prototype。
                dog instanceof Dog; // true
// 等价于：Dog.prototype 是否在 dog 的原型链中
                3. 如何实现对象继承？
构造函数继承（call/apply）、原型链继承、组合继承、寄生组合继承、Object.create()、ES6 类继承。
4. 原型链与类继承的区别：
类继承是静态的，实例与类关系固定；原型链是动态的，运行时可修改。
JS 的类本质是原型链的语法糖。
                `,
              }
            ]
          },
          {
            question: `8.说说你对作用域链的理解`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `作用域链（Scope Chain）的理解
在 JavaScript 中，作用域链是实现变量和函数查找的核心机制。它允许内部作用域访问外部作用域的变量，是理解 JS 变量查找、闭包等特性的基础。
1. 作用域（Scope）的基本概念
作用域：定义了变量和函数的可访问范围，控制着变量和函数的生命周期。
JavaScript 中的作用域类型：
全局作用域：最外层的作用域，所有未在函数或块级作用域中定义的变量都属于全局作用域。
函数作用域：每个函数都有自己的作用域，函数内部定义的变量只能在函数内部访问。
块级作用域（ES6+）：由 {} 包裹的代码块（如 if、for、while），使用 let 和 const 声明的变量会被限制在块级作用域内。
2. 作用域链的形成机制
每个执行上下文（Execution Context）都包含一个作用域链：
作用域链是一个由多个 ** 变量对象（Variable Object）** 组成的链表，它记录了变量和函数的定义位置。
变量对象：
全局执行上下文的变量对象是全局对象（浏览器中为 window）。
函数执行上下文的变量对象包含：
局部变量：函数内部定义的变量。
参数：函数的参数列表。
内部函数：函数内部定义的函数。
this 指针：指向当前执行上下文。
作用域链的构建：
每个执行上下文的作用域链由当前变量对象和外部作用域的作用域链组成。当创建一个函数时，它会保存一个对外部作用域的引用（即闭包的本质）。
3. 变量查找过程
当访问一个变量时，JavaScript 会：
先在当前执行上下文的变量对象中查找。
若未找到，则沿作用域链向上查找外部作用域的变量对象。
直到找到该变量或到达全局作用域（若仍未找到则报错 ReferenceError）。
示例：
const globalVar = 'global'; // 全局作用域变量

function outer() {
  const outerVar = 'outer'; // 外部函数作用域变量

  function inner() {
    const innerVar = 'inner'; // 内部函数作用域变量
    console.log(innerVar);    // 访问自身作用域变量
    console.log(outerVar);    // 沿作用域链向上查找 outerVar
    console.log(globalVar);   // 继续向上查找 globalVar
  }

  inner();
}

outer();
4. 闭包与作用域链
闭包：函数与其引用的外部变量的组合。即使外部函数执行完毕，闭包仍能访问外部函数的变量。
闭包的本质：闭包会捕获并保存其创建时的整个作用域链，即使外部函数已执行完毕，其变量对象也不会被销毁。
示例：
function createCounter() {
  let count = 0; // 闭包捕获的外部变量

  return function() {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1（访问闭包中的 count）
counter(); // 2（闭包中的 count 被保留）
5. 作用域链的特点
动态性：作用域链在运行时动态构建，基于函数的调用关系。
静态（词法）作用域：JavaScript 使用词法作用域（Lexical Scoping），即变量的作用域由其定义位置决定，而非调用位置。
const x = 10;

function foo() {
  console.log(x); // 10（由定义位置决定，与调用位置无关）
}

function bar() {
  const x = 20;
  foo(); // 调用 foo，但 x 仍从 foo 的定义位置查找
}

bar(); // 输出 10，而非 20
性能影响：多层级的作用域链会降低变量查找速度，因此应尽量减少嵌套层级。
`
              },
              {
                text: `6. 面试高频问题`,
                daima: `1. 词法作用域 vs 动态作用域：
词法作用域（JS 采用）：变量作用域由定义位置决定。
动态作用域（如 Bash）：变量作用域由调用位置决定。
2. 闭包如何影响内存？
闭包会保留对外部变量的引用，可能导致内存泄漏（如未释放 DOM 节点的闭包）。
3. 如何优化作用域链查找？f
减少嵌套层级，避免过深的作用域链。
使用局部变量缓存全局变量或外层变量（如 const doc = document）。
4. 作用域链与原型链的区别：
作用域链：用于查找变量和函数定义。
原型链：用于实现对象的继承和属性共享。`,
              }
            ]
          },
          {
            question: `9.谈谈this对象的理解`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、this 的核心特点
动态绑定：this 的值在函数调用时确定，而非定义时。
四种主要绑定规则：默认绑定、隐式绑定、显式绑定和 new 绑定。
箭头函数的特殊性：箭头函数不拥有自己的 this，而是继承自外层作用域。
1. 默认绑定
非严格模式：this指向全局对象，浏览器中指的是window
严格模式：this指向的是undefined
代码如下：
function sayHello() {
  console.log(this); // 非严格：window，严格：undefined
}
sayHello();
2. 隐式绑定，作为对象方法调用
this指向调用该函数的对象
const person = {
  name: "Alice",
  greet() {
    console.log('Hello, ￥{ this.name }'); // this 指向 person
  }
};
person.greet(); // 输出: "Hello, Alice"
3. 显示绑定，比如使用call，apply，bind
call/apply：立刻调用函数并指定this的值
bind：创建一个新函数，永久绑定this的值
4. new 绑定，构造函数调用
使用new调用函数时，this指向新创建的对象
特殊的this指向
箭头函数不绑定自己的this，而是继承自外层函数或者时全局作用域
常用于回调函数中保持this的上下文

面试中常见的坑
回调函数中的this
回调函数中的this通常会指向全局对象或者时undefined在严格模式下
代码如下：
const obj = {
  num: 10,
  callback() {
    console.log(this.num); // 输出: undefined
  }
};

setTimeout(obj.callback, 1000); // this 指向 window
解决方法，
使用箭头函数：setTimeout(() => obj.callback(), 1000)
使用 bind：setTimeout(obj.callback.bind(obj), 1000)

五、总结
理解 this 的关键在于记住它的绑定规则优先级：
new 绑定（最高优先级）
显式绑定（call/apply/bind）
隐式绑定（作为对象方法）
默认绑定（最低优先级）
对于箭头函数，记住它不绑定 this，而是继承自外层作用域。掌握这些规则可以避免大部分 this 相关的错误。
`
              }
            ]
          },
          {
            question: `10.说说new操作符具体干了什么?`,
            description: ``,
            answer: [
              {
                text: `一、new 操作符的执行流程
1. 创建新对象
首先创建一个空对象，该对象继承自构造函数的 prototype。
2. 绑定 this
将构造函数的 this 指向新创建的对象。
3. 执行构造函数
执行构造函数中的代码，为新对象添加属性和方法。
4. 返回对象
如果构造函数返回一个对象，则返回该对象；否则返回新创建的对象。`,
                daima: `手写new关键字
                function myNew(constructor, ...args) {
  // 1. 创建新对象，继承构造函数的 prototype
  const obj = Object.create(constructor.prototype);

  // 2. 绑定 this 并执行构造函数
  const result = constructor.apply(obj, args);

  // 3. 如果构造函数返回对象，则返回该对象；否则返回新创建的对象
  return typeof result === 'object' && result !== null ? result : obj;
}

// 使用示例
function Person(name) {
  this.name = name;
}

const alice = myNew(Person, "Alice");
console.log(alice.name); // 输出: "Alice"`
              },
              {
                text: `new 与 Object.create() 的区别`,
                daima: `new 会执行构造函数并创建实例。

Object.create() 仅创建对象并设置原型，不执行构造函数。`,
              },
              {
                text: `3. 构造函数返回值的影响`,
                daima: `返回对象：替代新创建的对象。

返回基本类型：忽略返回值，返回新创建的对象。`,
              }
            ]
          },
          {
            question: `11. bind、call、apply区别?如何实现一个bind?`,
            description: ``,
            answer: [
              {
                text: `关于这几个的区别坑定是知道的`,
                daima: ``
              },
              {
                text: `手写myCall函数`,
                daima: `    Function.prototype.myCall = function (thisArg, ...arrs) {
      let _this = thisArg === null || thisArg === undefined || typeof thisArg === "function" ? window : thisArg;
      const key = Symbol();
      _this[key] = this;
      let result = _this[key](...arrs);
      delete _this[key];
      return result;
    }`,
              },
              {
                text: `手动实现apply函数`,
                daima: `    Function.prototype.myApply = function (thisArg, arrs) {
      const key = Symbol();
      thisArg[key] = this;
      const res = thisArg[key](...arrs);
      delete thisArg[key];
      return res;
    }`,
              },
              {
                text: `手动实现bind函数`,
                daima: `    Function.prototype.myBind = function (thisArg, ...args) {
      return (...args2) => {
        return this.call(thisArg, ...args, ...args2)
      }
    }`,
              }
            ]
          },
          {
            question: `12.JavaScript中执行上下文和执行栈是什么?`,
            description: ``,
            answer: [
              {
                text: `这个js的执行上下文和执行栈是什么还是需要去找找对应的资料，我知道的那一套好像比较旧了，现在新的环境的说法是词法环境和变量环境`,
                daima: `一、执行上下文（Execution Context）
1. 定义
执行上下文是 JavaScript 中的一个抽象概念，它定义了变量和函数的作用域，并决定了代码的执行环境。每个执行上下文包含三个关键部分：
变量对象（Variable Object）：存储变量、函数声明和参数。
作用域链（Scope Chain）：由多个变量对象组成，用于查找变量。
this 指针：指向当前执行上下文的对象。
二、执行栈（Execution Stack）
1. 定义
执行栈（也称为调用栈）是 JavaScript 引擎用于管理执行上下文的一种数据结构，遵循 后进先出（LIFO） 的原则。
2. 工作流程
当 JavaScript 代码开始执行时，首先创建全局执行上下文并压入执行栈。
每次调用函数时，创建新的函数执行上下文并压入栈顶。
函数执行完毕后，其执行上下文从栈顶弹出，控制权返回给调用它的上下文。
全局执行上下文直到程序结束才会从栈中弹出。
三、变量提升（Hoisting）
变量提升是执行上下文创建阶段的产物：
函数声明：整个函数会被提升到作用域顶部，可以在声明前调用。
变量声明：仅变量名被提升，赋值留在原地。若在声明前访问，值为 undefined。
四、闭包与执行上下文
闭包是指有权访问另一个函数作用域中的变量的函数。即使该函数已执行完毕，其执行上下文被销毁，闭包仍能保留对原作用域的引用。`
              }
            ]
          },
          {
            question: `13.说说JavaScript中的事件模型`,
            description: ``,
            answer: [
              {
                text: `原始事件模型`,
                daima: `1. 特点
内联事件处理：直接在 HTML 标签中使用 onclick 等属性绑定事件。
DOM 对象属性：通过 JavaScript 直接设置元素的 onclick 等属性。
没有事件传播：事件不会在 DOM 树中传播，仅在绑定的元素上触发。
3. 优缺点
优点：兼容性好，简单直接。
缺点：一个元素只能绑定一个事件处理函数，无法实现事件委托。`
              },
              {
                text: `DOM2级事件模型，标准事件模型`,
                daima: `1. 特点
事件传播：事件分为三个阶段（捕获 → 目标 → 冒泡）。
事件监听：使用 addEventListener 方法绑定事件，支持多个处理函数。
事件取消：使用 removeEventListener 方法移除事件。
2. 事件传播的三个阶段
事件捕获（Capture Phase）：
事件从文档根节点（window → document → HTML → ...）向下传播到目标元素。
可以通过 addEventListener 的第三个参数 true 开启捕获阶段监听。
目标阶段（Target Phase）：
事件到达目标元素。
事件冒泡（Bubble Phase）：
事件从目标元素向上传播到文档根节点。
默认情况下，addEventListener 在冒泡阶段监听事件（第三个参数为 false 或省略）。

4. 事件委托（Event Delegation）
利用事件冒泡的特性，将事件处理函数绑定到父元素，通过 event.target 确定实际触发事件的元素。`,
              },
              {
                text: `IE事件模型，历史遗留问题`,
                daima: `1. 特点
仅支持冒泡阶段，不支持捕获阶段。
使用 attachEvent 和 detachEvent 方法绑定 / 移除事件。
this 指向 window，而非触发事件的元素。`,
              },
              {
                text: `事件对象EventObject`,
                daima: `当事件触发时，会创建一个事件对象（event），包含事件的相关信息：
event.target：实际触发事件的元素。
event.currentTarget：当前绑定事件的元素。
event.type：事件类型（如 click）。
event.preventDefault()：阻止默认行为（如链接跳转）。
event.stopPropagation()：停止事件传播。
event.stopImmediatePropagation()：停止后续所有事件处理函数的执行。`,
              }
            ]
          },
          {
            question: `14.解释下什么是事件代理？应用场景？`,
            description: ``,
            answer: [
              {
                text: `一、核心原理`,
                daima: `事件冒泡：当一个元素上的事件被触发时，该事件会从目标元素向上传播到父元素，直到文档根节点。
委托处理：将事件监听器绑定到父元素，通过 event.target 判断实际触发事件的子元素，执行相应操作。`
              },
              {
                text: `四、优缺点`,
                daima: `优点
减少内存占用：只需一个监听器处理多个元素。
动态适应性：新增子元素自动获得事件处理能力。
代码简洁：集中管理事件逻辑，减少重复代码。
缺点
不适用所有事件：仅适用于支持冒泡的事件（如 click、input），不支持 focus 等不冒泡的事件。
事件处理复杂度：多层嵌套时，需谨慎处理 event.target 和 event.currentTarget 的区别。`,
              },
              {
                text: `其他常见的面试题之，手写事件委托实现`,
                daima: `function delegate(parent, selector, eventType, handler) {
  parent.addEventListener(eventType, (e) => {
    if (e.target.matches(selector)) {
      handler(e);
    }
  });
}

// 使用示例
delegate(
  document.getElementById('container'),
  'button',
  'click',
  (e) => console.log('Button clicked')
);`,
              },
              {
                text: `2. 事件委托 vs 直接绑定`,
                daima: `对比项	事件委托	直接绑定
监听器数量	少（父元素一个）	多（每个子元素一个）
动态元素支持	自动支持	需要重新绑定
内存占用	低	高
适用场景	大量相似元素、动态内容	少量元素、独立事件处理`,
              },
              {
                text: `3. 事件委托的限制`,
                daima: `不支持不冒泡的事件（如 focus、blur）。
可能增加事件处理的复杂度，特别是嵌套层级较深时。`,
              }
            ]
          },
          {
            question: `15.说说你对闭包的理解？闭包使用场景`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、闭包的本质
定义：闭包是指有权访问另一个函数作用域中的变量的函数。
核心机制：即使外部函数已经执行完毕，其作用域内的变量也不会被销毁，而是会被闭包捕获并保留引用。
function outer() {
  const x = 10;
  function inner() { // inner 是一个闭包
    console.log(x); // 闭包捕获了 outer 函数的变量 x
  }
  return inner;
}

const closure = outer();
closure(); // 输出: 10（即使 outer 已执行完毕）
二、闭包的三大核心特性
1. 捕获变量
闭包会捕获其定义时所在作用域中的所有变量，而非仅仅是变量的值。
javascript
function createCounter() {
  let count = 0;
  return function() {
    count++; // 闭包捕获并修改 count 变量
    console.log(count);
  };
}

const counter1 = createCounter();
counter1(); // 输出: 1
counter1(); // 输出: 2

const counter2 = createCounter();
counter2(); // 输出: 1（独立的闭包实例）
2. 保留状态
闭包可以保留其创建时的状态，形成类似 “私有变量” 的效果。
javascript
function createPerson(name) {
  return {
    getName: function() {
      return name; // 闭包捕获 name 变量
    },
    setName: function(newName) {
      name = newName; // 修改闭包捕获的变量
    }
  };
}

const person = createPerson("Alice");
console.log(person.getName()); // 输出: "Alice"
person.setName("Bob");
console.log(person.getName()); // 输出: "Bob"
3. 延迟执行
闭包中的变量值会在函数执行时被解析，而非定义时。
javascript
function delayedLog() {
  const messages = ["Hello", "World"];
  for (var i = 0; i < messages.length; i++) {
    setTimeout(() => {
      console.log(messages[i]); // 闭包捕获的是共享的 i 变量
    }, 1000);
  }
}

delayedLog(); // 输出: undefined（两次）
// 原因：循环结束时 i 的值为 2，所有定时器执行时都访问到这个值
修正方法：使用 let（块级作用域）或立即执行函数：
javascript
for (let i = 0; i < messages.length; i++) { // let 创建块级作用域
  setTimeout(() => {
    console.log(messages[i]); // 正确输出: "Hello" "World"
  }, 1000);
}
三、闭包的经典应用场景
1. 模块化与私有变量
创建具有私有状态的模块，避免全局变量污染。
javascript
const counterModule = (() => {
  let count = 0; // 私有变量
  return {
    increment: () => count++,
    getCount: () => count
  };
})();

console.log(counterModule.getCount()); // 0
counterModule.increment();
console.log(counterModule.getCount()); // 1
2. 事件处理与回调函数
在事件处理函数中保留上下文状态。
javascript
function setupButton() {
  const text = "Button Clicked";
  document.getElementById("btn").addEventListener("click", () => {
    alert(text); // 闭包捕获 text 变量
  });
}
setupButton();
3. 函数柯里化（Currying）
将多参数函数转换为单参数函数序列。
javascript
function add(a, b) {
  return a + b;
}

// 柯里化版本
const curriedAdd = a => b => a + b;
console.log(curriedAdd(3)(5)); // 输出: 8
4. 实现高阶函数
动态生成具有特定行为的函数。
javascript
function multiplyBy(factor) {
  return num => num * factor; // 闭包捕获 factor 变量
}

const double = multiplyBy(2);
console.log(double(5)); // 输出: 10
5. 模拟块级作用域（ES5 时代）
在 ES6 之前，使用闭包模拟 let 的块级作用域。
javascript
for (var i = 0; i < 3; i++) {
  (function(j) { // 立即执行函数创建闭包
    setTimeout(() => {
      console.log(j); // 正确输出: 0 1 2
    }, 1000);
  })(i);
}

四、闭包的优缺点
优点
数据封装：实现私有变量和方法，避免全局污染。
状态持久化：保留函数执行时的状态。
函数复用：通过闭包动态生成特定行为的函数。
缺点
内存占用：闭包会保留对外部变量的引用，可能导致内存泄漏。
性能开销：频繁创建闭包可能影响性能。
调试困难：闭包的变量作用域链可能复杂，增加调试难度。
`
              },
              {
                text: `其他关于闭包的问题`,
                daima: `
                1. 手写闭包示例
javascript
function createAdder(x) {
  return function(y) {
    return x + y; // 闭包捕获 x 变量
  };
}

const add5 = createAdder(5);
console.log(add5(3)); // 输出: 8
2. 闭包与内存泄漏
内存泄漏场景：如果闭包保留了对 DOM 元素的引用，即使元素被移除，内存也不会被释放。
解决方案：在不需要闭包时手动释放引用（如设置为 null）。
3. 闭包在循环中的陷阱
javascript
// 错误示例
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000); // 输出: 3 3 3
}

// 正确示例（使用 let）
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000); // 输出: 0 1 2
}
`,
              }
            ]
          },
          {
            question: `16.谈谈JavaScript中的类型转换机制`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、类型转换的分类
JavaScript 中的类型转换分为两种：
显式转换（强制类型转换）：通过内置函数手动转换类型。
隐式转换（自动类型转换）：在运算、比较或特定操作中自动发生。
二、显式类型转换（强制转换）
通过调用特定函数实现类型转换：
1. 转换为数字
Number()：通用转换函数
parseInt(string, radix)：解析字符串为整数
parseFloat(string)：解析字符串为浮点数
javascript
// Number() 的转换规则
Number("123");      // 123
Number("12.3");     // 12.3
Number("abc");      // NaN
Number(true);       // 1
Number(false);      // 0
Number(null);       // 0
Number(undefined);  // NaN
Number([]);         // 0
Number([1]);        // 1
Number([1, 2]);     // NaN (无法转换)
2. 转换为字符串
String()：通用转换函数
toString()：对象方法（null 和 undefined 不可用）
javascript
String(123);        // "123"
String(true);       // "true"
String(null);       // "null"
String(undefined);  // "undefined"

const obj = { a: 1 };
obj.toString();     // "[object Object]"
3. 转换为布尔值
Boolean()：将值转换为布尔类型
falsy 值：false, 0, "", null, undefined, NaN
truthy 值：其他所有值（包括空对象 {}、空数组 []）
javascript
Boolean(0);         // false
Boolean("");        // false
Boolean(null);      // false
Boolean(undefined); // false
Boolean(NaN);       // false

Boolean({});        // true
Boolean([]);        // true
Boolean("0");       // true (非空字符串)


三、隐式类型转换（自动转换）
在运算、比较或特定操作中，JavaScript 会自动进行类型转换。
1. 算术运算符中的隐式转换
加法运算符（+）：
若任一操作数是字符串，则将另一个转换为字符串并拼接。
否则，将操作数转换为数字进行计算。
javascript
"1" + 2;      // "12" (字符串拼接)
1 + true;     // 2 (true → 1)
1 + null;     // 1 (null → 0)
1 + undefined; // NaN (undefined → NaN)
其他运算符（-, *, /, %）：
将操作数转换为数字进行计算。
javascript
"5" - 2;      // 3 (字符串 → 5)
"abc" - 1;    // NaN (无法转换)
null * 10;    // 0 (null → 0)
2. 比较运算符中的隐式转换
宽松相等（== 和 !=）：允许类型转换后比较。
严格相等（=== 和 !==）：不允许类型转换。
javascript
1 == "1";     // true (字符串 → 1)
1 === "1";    // false (类型不同)
null == undefined; // true (特殊规则)
null === undefined; // false

0 == false;   // true (both → 0)
0 === false;  // false

[] == 0;      // true ([].valueOf().toString() → "" → 0)
3. 布尔上下文中的隐式转换
在条件判断（如 if, while）中，值会被自动转换为布尔类型。
javascript
if ("hello") {
  // 执行 (非空字符串 → true)
}

if ([]) {
  // 执行 (空数组 → true)
}

if (0) {
  // 不执行 (0 → false)
}
4. 对象的隐式转换
对象在参与运算或比较时，会先转换为原始值：
调用 valueOf() 方法。
若 valueOf() 返回非原始值，则调用 toString() 方法。
javascript
const obj = {
  valueOf: () => 10,
  toString: () => "hello"
};

obj + 5;      // 15 (优先使用 valueOf())
obj == 10;    // true
四、特殊转换规则
null 和 undefined：
javascript
Number(null);      // 0
Number(undefined); // NaN
null == undefined; // true
null === undefined; // false

NaN：
javascript
NaN === NaN;       // false (唯一不等于自身的值)
isNaN(NaN);        // true
isNaN("abc");      // true (隐式转换为 NaN)
Number.isNaN("abc"); // false (不进行隐式转换)

+ 与 - 的区别：
javascript
+"123";     // 123 (转换为数字)
-"123";     // -123

五、常见面试问题
1. 解释 [] == ![] 的结果
javascript
[] == ![]; // true

// 解析步骤：
// 1. ![] → false (数组为 truthy 值，取反为 false)
// 2. [] == false → 转换为数字比较
// 3. [].valueOf().toString() → ""
// 4. "" → 0, false → 0
// 5. 0 == 0 → true
2. 如何实现自定义对象的类型转换
通过重写 valueOf() 或 toString() 方法：
javascript
const obj = {
  valueOf: () => 20,
  toString: () => "obj"
};

obj + 5;      // 25 (优先使用 valueOf())
String(obj);  // "obj"
3. Object.is() 与 === 的区别
javascript
Object.is(0, -0);       // false (0 === -0 → true)
Object.is(NaN, NaN);    // true (NaN === NaN → false)
Object.is(5, "5");      // false

六、总结
JavaScript 的类型转换机制灵活但复杂，核心要点包括：
显式转换：使用 Number(), String(), Boolean() 等函数。
隐式转换：在运算和比较中自动发生，需注意 + 运算符的字符串拼接特性。
宽松相等（==）：会进行类型转换，规则复杂。
严格相等（===）：推荐使用，避免意外转换。
对象转换：通过 valueOf() 和 toString() 控制。
理解这些规则可以避免常见的类型相关错误，提升代码的健壮性。
`
              }
            ]
          },
          {
            question: `17.深拷贝浅拷贝的区别？如何实现一个深拷贝？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、深拷贝 vs 浅拷贝的核心区别
对比项	浅拷贝（Shallow Copy）	深拷贝（Deep Copy）
对象层级	只复制对象的第一层属性	递归复制对象的所有层级属性
引用类型处理	复制引用（新旧对象共享同一内存地址）	创建新对象并递归复制其所有属性
修改影响	修改引用类型属性会影响原对象	修改新对象不会影响原对象
实现难度	简单（如 Object.assign()、展开运算符）	复杂（需处理循环引用、特殊对象类型）
二、浅拷贝的实现方式
手动遍历：复制第一层属性
Object.assign()：复制所有可枚举属性
展开运算符（...）：创建新对象并复制属性
Array.prototype.slice() 或 [...arr]：数组浅拷贝
javascript
// 示例：浅拷贝
const original = {
  a: 1,
  b: { c: 2 }
};

const shallowCopy = { ...original }; // 展开运算符

shallowCopy.b.c = 100;
console.log(original.b.c); // 100（原对象被修改）
三、深拷贝的实现思路
1. 基础递归实现
遍历对象的所有属性，递归复制每个属性：
javascript
function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj; // 原始值直接返回
  }
  
  const copy = Array.isArray(obj) ? [] : {};
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key]); // 递归复制
    }
  }
  
  return copy;
}
2. 处理循环引用
使用 WeakMap 存储已复制的对象，避免循环引用导致的无限递归：
javascript
function deepCopy(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 检查循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }
  
  const copy = Array.isArray(obj) ? [] : {};
  
  // 存储当前对象到 map
  map.set(obj, copy);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key], map);
    }
  }
  
  return copy;
}
3. 处理特殊对象类型
对于 Date、RegExp、Map、Set 等特殊对象，需要使用特定方法复制：
javascript
function deepCopy(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理特殊对象类型
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  
  // 处理循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }
  
  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);
  
  // 处理 Map
  if (obj instanceof Map) {
    obj.forEach((value, key) => {
      copy.set(key, deepCopy(value, map));
    });
    return copy;
  }
  
  // 处理 Set
  if (obj instanceof Set) {
    obj.forEach(value => {
      copy.add(deepCopy(value, map));
    });
    return copy;
  }
  
  // 处理普通对象和数组
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key], map);
    }
  }
  
  return copy;
}
四、常见深拷贝工具函数
1. JSON.parse(JSON.stringify())
简单但有局限性（无法处理函数、undefined、Symbol、循环引用等）：
javascript
const deepCopy = JSON.parse(JSON.stringify(original));
2. Lodash 的 _.cloneDeep()
功能完善的深拷贝实现，处理各种边界情况：
javascript
import _ from 'lodash';
const deepCopy = _.cloneDeep(original);
五、深拷贝的局限性
性能开销：递归复制所有层级，对大型对象效率低。
特殊对象处理：无法复制函数、DOM 节点等特殊对象。
循环引用：必须使用额外机制（如 WeakMap）避免无限递归。
六、常见面试问题
1. 手写深拷贝函数
参考上述递归实现，需包含：
基础类型直接返回
对象 / 数组类型递归处理
循环引用检测（使用 WeakMap）
特殊对象类型处理（Date、RegExp 等）
2. 为什么不能用 JSON.parse(JSON.stringify()) 实现深拷贝？
会丢失 undefined、Symbol 和函数
无法处理循环引用
无法正确复制 Date、RegExp 等特殊对象
3. 深拷贝和浅拷贝的应用场景
浅拷贝：适用于单层对象，或不需要修改引用类型属性的场景。
深拷贝：需要完全独立副本的场景（如状态管理、数据缓存）。
七、总结
浅拷贝：复制对象的一层属性，引用类型共享内存。
深拷贝：递归复制所有层级，创建完全独立的对象。
实现要点：递归处理嵌套对象、检测循环引用、处理特殊对象类型。
工具选择：优先使用成熟库（如 Lodash），简单场景可用 JSON 方法。`
              }
            ]
          },
          {
            question: `18.Javascript中如何实现函数缓存?函数缓存有哪些应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、函数缓存的核心原理
函数缓存是一种优化技术，通过存储函数的输入参数和对应结果，避免重复计算。其核心原理是：
记忆化（Memoization）：在函数调用时，先检查缓存中是否已有结果。
键值对存储：使用对象或 Map 存储参数与结果的映射关系。
参数哈希：将参数转换为唯一键（如字符串化），确保相同参数对应相同缓存键。
二、实现方式
1. 基础实现（单参数函数）
javascript
function memoize(func) {
  const cache = new Map();
  return function(arg) {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    const result = func(arg);
    cache.set(arg, result);
    return result;
  };
}

// 使用示例
const expensiveFunc = (n) => {
  console.log('Calculating...');
  return n * n;
};

const memoized = memoize(expensiveFunc);
console.log(memoized(5)); // 计算并缓存: 25
console.log(memoized(5)); // 直接返回缓存: 25
2. 多参数函数的缓存
将参数数组转换为字符串作为缓存键：
javascript
function memoize(func) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = func.apply(this, args);
    cache.set(key, result);
    return result;
  };
}
3. 处理对象参数
使用自定义哈希函数处理复杂参数：
javascript
function createHash(args) {
  return args.map(arg => 
    typeof arg === 'object' ? JSON.stringify(arg) : arg
  ).join('|');
}

function memoize(func) {
  const cache = new Map();
  return function(...args) {
    const key = createHash(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = func.apply(this, args);
    cache.set(key, result);
    return result;
  };
}
4. 带过期时间的缓存
javascript
function memoizeWithExpiry(func, expiryTime = 60000) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < expiryTime) {
      return cached.value;
    }
    
    const result = func.apply(this, args);
    cache.set(key, { value: result, timestamp: Date.now() });
    return result;
  };
}
三、应用场景
1. 计算密集型函数
如斐波那契数列、阶乘等递归函数：
javascript
const fib = memoize((n) => {
  if (n <= 1) return n;
  return fib(n-1) + fib(n-2);
});

console.log(fib(10)); // 首次计算
console.log(fib(10)); // 直接从缓存获取
2. API 请求缓存
避免重复请求相同数据：
javascript
const fetchUser = memoizeWithExpiry(async (userId) => {
  const response = await fetch('/ api / users / ￥{ userId }');
  return response.json();
}, 300000); // 5分钟缓存
3. UI 组件渲染优化
在 React 中使用 React.memo 缓存组件：
javascript
const MyComponent = React.memo((props) => {
  // 仅在 props 变化时重新渲染
});
4. 配置解析
缓存复杂配置的解析结果：
javascript
const parseConfig = memoize((config) => {
  // 复杂的配置解析逻辑
  return parsedConfig;
});
5. 模板编译
缓存编译后的模板函数：
javascript
const compileTemplate = memoize((template) => {
  return Handlebars.compile(template);
});
四、优缺点
优点
提升性能：减少重复计算，尤其适用于耗时操作。
降低资源消耗：减少网络请求或 CPU 计算。
简化代码：避免手动管理缓存状态。
缺点
内存占用：缓存数据可能占用大量内存。
缓存失效：数据更新时需手动清除缓存。
参数限制：复杂参数（如函数、对象引用）可能导致缓存键冲突。
五、常见面试问题
1. 手写一个函数缓存实现
需包含：
缓存容器（Map 或对象）
参数序列化（如 JSON.stringify）
缓存命中逻辑
2. 如何处理对象参数的缓存
使用 JSON.stringify 转换为字符串
或实现自定义哈希函数（如递归序列化对象）
3. 函数缓存与本地存储的区别
对比项	函数缓存	本地存储（LocalStorage）
存储位置	内存（临时）	浏览器存储（持久化）
数据类型	任意 JavaScript 对象	字符串
生命周期	页面刷新后失效	长期保存
应用场景	函数结果复用	跨会话数据存储
4. 如何处理缓存过期和失效
添加时间戳，定期检查
提供手动清除缓存的接口
使用 LRU（最近最少使用）算法淘汰旧缓存
六、总结
函数缓存通过存储输入 - 输出映射提升性能，适用于计算密集、结果稳定的函数。实现时需注意参数序列化、内存管理和缓存失效策略。在前端开发中，它常用于优化递归计算、API 请求和组件渲染。`
              }
            ]
          },
          {
            question: `19.JavaScript字符串的常用方法有哪些?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，字符串是基本数据类型，提供了丰富的内置方法。以下是面试中高频出现的字符串方法分类总结：
一、字符串长度与访问
length 属性
返回字符串的长度（字符个数）。
javascript
const str = "hello";
console.log(str.length); // 5

charAt(index)
返回指定位置的字符。
javascript
str.charAt(1); // "e"
str[1];        // "e"（方括号语法，ES5+）

二、查找与匹配
indexOf(searchValue, fromIndex)
返回子字符串首次出现的位置，未找到返回 -1。
javascript
"hello".indexOf("l"); // 2
"hello".indexOf("l", 3); // 3

lastIndexOf(searchValue, fromIndex)
返回子字符串最后一次出现的位置。
javascript
"hello".lastIndexOf("l"); // 3

includes(searchString, position)
判断字符串是否包含指定子串（ES6+）。
javascript
"hello".includes("ell"); // true

startsWith(searchString, position) 和 endsWith(searchString, position)
判断字符串是否以指定子串开头 / 结尾（ES6+）。
javascript
"hello".startsWith("he"); // true
"hello".endsWith("lo");   // true

match(regexp)
返回匹配正则表达式的结果数组。
javascript
"hello".match(/l/g); // ["l", "l", "l"]

search(regexp)
返回第一个匹配项的索引，未找到返回 -1。
javascript
"hello".search(/l/); // 2

三、截取与拼接
slice(start, end)
提取字符串的一部分（支持负索引，表示从末尾开始）。
javascript
"hello".slice(1, 3); // "el"
"hello".slice(-2);  // "lo"

substring(start, end)
类似 slice，但不支持负索引。
javascript
"hello".substring(1, 3); // "el"
substr(start, length)
提取从 start 开始的 length 个字符（ES5 及以前，不推荐新代码使用）。
javascript
"hello".substr(1, 2); // "el"
concat(str1, str2, ...)
拼接多个字符串，等价于 + 操作符。
javascript
"hello".concat(" ", "world"); // "hello world"
四、转换与替换
toLowerCase() 和 toUpperCase()
转换为小写 / 大写。
javascript
"Hello".toLowerCase(); // "hello"
replace(regexp|substr, newSubstr|function)
替换匹配的子串（正则支持全局替换）。
javascript
"hello".replace("l", "x");    // "heixo"（仅替换第一个）
"hello".replace(/l/g, "x");   // "hexxo"（全局替换）
split(separator, limit)
将字符串分割为数组。
javascript
"hello".split("");     // ["h", "e", "l", "l", "o"]
"a,b,c".split(",");    // ["a", "b", "c"]
五、修剪与填充
trim()
移除字符串首尾的空白字符（ES5+）。
javascript
"  hello  ".trim(); // "hello"
padStart(targetLength, padString) 和 padEnd(targetLength, padString)
用指定字符串从左侧 / 右侧填充至目标长度（ES2017+）。
javascript
"5".padStart(3, "0"); // "005"
"hello".padEnd(7, "*"); // "hello**"
六、编码与解码
encodeURI(uri) 和 decodeURI(uri)
对整个 URI 进行编码 / 解码，保留特殊字符（如 /、?）。
javascript
encodeURI("https://example.com?name=张三");
// "https://example.com?name=%E5%BC%A0%E4%B8%89"
encodeURIComponent(uriComponent) 和 decodeURIComponent(uriComponent)
对 URI 组件进行编码 / 解码，编码所有特殊字符。
javascript
encodeURIComponent("name=张三&age=20");
// "name%3D%E5%BC%A0%E4%B8%89%26age%3D20"
七、其他常用方法
repeat(count)
重复字符串指定次数（ES6+）。
javascript
"a".repeat(3); // "aaa"
localeCompare(compareString, locales, options)
比较两个字符串在当前语言环境下的排序顺序。
javascript
"a".localeCompare("b"); // -1
八、常见面试问题
手写 trim() 方法
javascript
function trim(str) {
  return str.replace(/^\s+|\s+$/g, '');
}

如何判断字符串包含子串？
方法：includes()、indexOf()、正则表达式
区别：includes() 返回布尔值，indexOf() 返回位置（需检查是否 -1）
字符串反转
javascript
"hello".split('').reverse().join(''); // "olleh"

统计字符串中某个字符的出现次数
javascript
function countChar(str, char) {
  return str.split(char).length - 1;
}


九、总结
掌握这些方法可以高效处理字符串操作，常见考点包括：
查找与匹配：indexOf、includes、正则相关方法
截取与拼接：slice、substring、concat
转换与替换：toLowerCase、replace
边界场景：空字符串、特殊字符、性能优化
建议结合正则表达式一起复习，提升字符串处理能力。`
              }
            ]
          },
          {
            question: `20.数组的常用方法有哪些?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，数组方法是面试的高频考点。以下是按功能分类的核心数组方法总结，附带代码示例和面试常见问题：
一、修改原数组的方法（重点）
1. 增删元素
push(...items)：末尾添加元素，返回新长度。
javascript
const arr = [1, 2];
arr.push(3); // [1, 2, 3]

pop()：删除并返回最后一个元素。
shift()：删除并返回第一个元素。
unshift(...items)：开头添加元素，返回新长度。
splice(start, deleteCount, ...items)：插入 / 删除元素。
javascript
arr.splice(1, 0, 'a'); // [1, 'a', 2, 3]

2. 排序与反转
sort(compareFn)：排序（注意默认按字符串排序）。
javascript
[3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]

reverse()：反转数组。
3. 填充与复制
fill(value, start, end)：填充指定范围。
javascript
[1, 2, 3].fill(0, 1, 2); // [1, 0, 3]

copyWithin(target, start, end)：复制内部元素到指定位置。
二、返回新数组的方法（重点）
1. 映射与过滤
map(callback)：对每个元素执行操作，返回新数组。
javascript
[1, 2].map(x => x * 2); // [2, 4]

filter(callback)：过滤符合条件的元素。
javascript
[1, 2, 3].filter(x => x > 1); // [2, 3]

2. 切片与拼接
slice(start, end)：返回子数组（不包含 end）。
javascript
[1, 2, 3].slice(1, 2); // [2]

concat(...arrays)：合并数组。
javascript
[1].concat([2, 3]); // [1, 2, 3]

3. 扁平化
flat(depth)：扁平化嵌套数组（ES2019+）。
javascript
[1, [2, [3]]].flat(2); // [1, 2, 3]

flatMap(callback)：先映射后扁平化。
三、查找与判断方法
1. 查找元素
find(callback)：返回第一个匹配的元素。
javascript
[1, 2, 3].find(x => x > 1); // 2

findIndex(callback)：返回匹配元素的索引。
indexOf(value) 和 lastIndexOf(value)：返回首次 / 最后一次出现的索引。
includes(value)：判断是否包含值（区分 NaN）。
2. 条件判断
every(callback)：所有元素满足条件返回 true。
some(callback)：至少一个元素满足条件返回 true。
四、归约与迭代
1. 归约
reduce(callback, initialValue)：从左到右累计计算。
javascript
[1, 2, 3].reduce((sum, x) => sum + x, 0); // 6

reduceRight(callback, initialValue)：从右到左累计计算。
2. 迭代
forEach(callback)：遍历数组（无返回值）。
entries()、keys()、values()：返回迭代器。
javascript
[...[1, 2].entries()]; // [[0, 1], [1, 2]]

五、转换与其他方法
1. 类型转换
join(separator)：数组转字符串。
javascript
[1, 2].join('-'); // "1-2"

Array.from(arrayLike)：类数组对象转数组。
javascript
Array.from('abc'); // ['a', 'b', 'c']

2. 排序稳定性
ES2019+ 保证 sort() 是稳定排序（相同元素保持原有顺序）。
六、常见面试问题
1. 如何去重数组？
javascript
const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]
2. map 和 forEach 的区别？
map 返回新数组，forEach 无返回值。
map 用于转换数据，forEach 用于副作用操作。
3. 手写 flat 方法
javascript
function flat(arr, depth = 1) {
  return depth > 0
    ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flat(val, depth - 1) : val), [])
    : arr.slice();
}
4. 如何判断数组包含特定值？
includes：直接判断值（支持 NaN）。
indexOf：需检查返回值是否 -1（不支持 NaN）。
some：自定义条件判断。
七、总结
重点掌握：
修改原数组：push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin
返回新数组：map/filter/slice/concat/flat/flatMap
查找与判断：find/findIndex/includes/every/some
归约：reduce/reduceRight
理解方法的副作用（是否修改原数组）和返回值类型是关键！`
              }
            ]
          },
          {
            question: `21. 说说你对事件循环的理解`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `事件循环（Event Loop）是 JavaScript 的核心执行机制，用于处理异步操作。理解事件循环对于编写高性能、无阻塞的代码至关重要。以下是深度解析：
一、JavaScript 的执行环境
JavaScript 是单线程、非阻塞的语言，其执行环境包含三个核心组件：
调用栈（Call Stack）：同步任务的执行栈。
任务队列（Task Queue）：异步任务完成后等待执行的队列。
事件循环（Event Loop）：不断从任务队列中取出任务并压入调用栈执行。
二、事件循环的核心机制
1. 调用栈与任务队列的交互
同步任务：直接在调用栈中执行。
异步任务：
发起异步操作（如 setTimeout、网络请求）。
异步操作进入浏览器 API 或 Node.js 环境处理。
操作完成后，回调函数被放入任务队列。
事件循环不断检查调用栈是否为空，若为空则从任务队列取任务执行。
2. 宏任务（MacroTask）与微任务（MicroTask）
任务队列分为两类：
宏任务：setTimeout、setInterval、setImmediate（Node.js）、requestAnimationFrame（浏览器）、I/O 操作等。
微任务：Promise.then、async/await（本质是 Promise）、MutationObserver（浏览器）、process.nextTick（Node.js）。
执行顺序：
调用栈清空后，优先执行所有微任务队列中的任务。
微任务队列清空后，执行一个宏任务。
重复步骤 1 和 2。
三、事件循环执行流程
javascript
console.log('Start');

setTimeout(() => {
  console.log('Timer 1');
  Promise.resolve().then(() => console.log('Promise in Timer 1'));
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
  Promise.resolve().then(() => console.log('Promise 2'));
});

console.log('End');
执行顺序解析：
同步任务：
plaintext
Start → End

微任务：
plaintext
Promise 1 → Promise 2

宏任务：
plaintext
Timer 1 → Promise in Timer 1（Timer 1 的微任务）

最终输出：
plaintext
Start
End
Promise 1
Promise 2
Timer 1
Promise in Timer 1
四、浏览器与 Node.js 的差异
1. 浏览器环境
事件循环由浏览器实现，主要处理 DOM 操作、网络请求等。
宏任务和微任务队列独立，执行顺序严格遵循：宏任务 → 所有微任务 → 下一宏任务。
2. Node.js 环境
事件循环分为多个阶段（Timers → I/O callbacks → idle, prepare → Poll → Check → Close callbacks）。
process.nextTick：特殊微任务，在每个阶段结束后立即执行，优先级高于其他微任务。
setImmediate：在 Check 阶段执行，优先级低于 setTimeout。
Node.js 示例：
javascript
setTimeout(() => {
  console.log('Timeout');
}, 0);

setImmediate(() => {
  console.log('Immediate');
});

process.nextTick(() => {
  console.log('NextTick');
});
执行顺序：
plaintext
NextTick → (Timeout 或 Immediate，取决于初始化时间)
五、常见面试问题
1. 解释事件循环的工作原理
事件循环是 JavaScript 的执行机制，负责协调同步和异步任务。
它不断检查调用栈是否为空，若为空则从任务队列中取出任务执行。
微任务队列优先级高于宏任务队列，每次调用栈清空后优先处理所有微任务。
2. 宏任务和微任务的区别
对比项	宏任务（MacroTask）	微任务（MicroTask）
示例	setTimeout, setInterval	Promise.then, process.nextTick
执行时机	调用栈清空后，每次执行一个	调用栈清空后，优先执行所有微任务
优先级	低于微任务	高于宏任务
3. 分析代码执行顺序
javascript
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

async1();

new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');
执行顺序解析：
同步任务：
plaintext
script start → async1 start → async2 → promise1 → script end

微任务：
plaintext
async1 end → promise2

宏任务：
plaintext
setTimeout

最终输出：
plaintext
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

六、总结
事件循环的核心是：
单线程执行：JavaScript 同一时间只能执行一个任务。
异步非阻塞：通过任务队列处理异步操作，避免阻塞主线程。
微任务优先：微任务队列在每次调用栈清空后优先执行。
理解事件循环有助于编写无阻塞的代码，避免常见的异步陷阱（如回调地狱），并优化应用性能。`
              }
            ]
          },
          {
            question: `22.Javascript本地存储的方式有哪些？区别及应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，本地存储是在浏览器端持久化数据的重要手段。以下是常见的本地存储方式及其对比：
一、主要存储方式
1. Cookie
特性：
数据会随 HTTP 请求发送到服务器端。
大小限制约 4KB。
可设置过期时间（expires/max-age）。
路径（path）和域（domain）限制。
应用场景：
身份验证（如 session ID）。
跟踪用户行为（如页面访问次数）。
示例：
javascript
// 设置 Cookie
document.cookie = "username=John; expires=Thu, 01 Jan 2025 00:00:00 UTC; path=/";

// 读取 Cookie
const cookies = document.cookie.split('; ').reduce((acc, cookie) => {
  const [key, value] = cookie.split('=');
  acc[key] = value;
  return acc;
}, {});

2. LocalStorage
特性：
数据永久存储（除非手动清除）。
大小限制约 5-10MB。
仅在浏览器端访问，不随 HTTP 请求发送。
同源策略限制（不同域名无法共享）。
应用场景：
缓存静态数据（如用户偏好设置）。
离线应用数据存储。
示例：
javascript
// 存储数据
localStorage.setItem('theme', 'dark');

// 读取数据
const theme = localStorage.getItem('theme');

// 删除数据
localStorage.removeItem('theme');

3. SessionStorage
特性：
数据仅在当前会话期间有效（页面关闭后清除）。
其他特性与 LocalStorage 相同。
应用场景：
临时保存表单数据。
会话级缓存（如多步骤表单进度）。
4. IndexedDB
特性：
基于数据库的存储（支持事务和索引）。
理论无大小限制（受硬盘空间约束）。
异步 API（避免阻塞主线程）。
支持存储结构化数据（如对象、文件）。
应用场景：
大量数据存储（如图库、离线应用）。
复杂查询需求（如基于索引检索）。
示例：
javascript
const request = indexedDB.open('myDatabase', 1);

request.onsuccess = (event) => {
  const db = event.target.result;
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  store.add({ id: 1, name: 'John' });
};

5. Web SQL (已弃用)
特性：
基于 SQL 的数据库 API。
已被 W3C 弃用，不建议使用。
二、核心区别对比
特性	Cookie	LocalStorage	SessionStorage	IndexedDB
存储大小	~4KB	~5-10MB	~5-10MB	无明确限制
数据有效期	可设置过期时间	永久存储	会话结束清除	永久存储
数据类型	字符串	字符串	字符串	任意 JavaScript 对象
服务器通信	随请求发送	仅客户端	仅客户端	仅客户端
API 类型	原生但操作繁琐	简单键值对	简单键值对	复杂（数据库操作）
跨页面共享	同源所有页面	同源所有页面	仅当前会话	同源所有页面
异步支持	同步	同步	同步	异步
三、应用场景选择
需要服务器交互的数据 → Cookie
如身份验证 token，自动随请求发送到服务器。
小量静态数据缓存 → LocalStorage
如主题设置、用户偏好，减少重复请求。
临时会话数据 → SessionStorage
如多步骤表单数据，页面刷新后保留。
大量结构化数据 → IndexedDB
如图库应用、离线版 CRM 系统。
频繁读写的小数据 → LocalStorage
如缓存 API 响应结果。
四、常见面试问题
1. Cookie 和 LocalStorage 的主要区别
数据大小：Cookie 仅 4KB，LocalStorage 约 5-10MB。
服务器通信：Cookie 自动随请求发送，LocalStorage 仅客户端可用。
有效期：Cookie 可设置过期时间，LocalStorage 永久存储。
2. 如何实现跨标签页通信
LocalStorage 监听：
javascript
// 页面 A 设置数据
localStorage.setItem('message', 'Hello from A');

// 页面 B 监听变化
window.addEventListener('storage', (event) => {
  if (event.key === 'message') {
    console.log('Received:', event.newValue);
  }
});

3. IndexedDB 与 LocalStorage 的对比
数据结构：LocalStorage 仅支持字符串，IndexedDB 支持复杂对象。
存储容量：IndexedDB 无明确限制，适合大量数据。
API 复杂度：IndexedDB 需学习数据库操作（如事务、游标）。
4. 如何安全地使用 Cookie
使用 HttpOnly 防止 XSS 攻击。
使用 Secure 标记确保仅通过 HTTPS 传输。
设置合理的 path 和 domain 限制。
五、总结
选择本地存储方式时需考虑：
数据大小：Cookie 适合小数据，IndexedDB 适合大数据。
有效期：临时数据用 SessionStorage，永久数据用 LocalStorage/IndexedDB。
服务器交互需求：需要与服务器通信时使用 Cookie。
数据复杂度：结构化数据优先选择 IndexedDB。
合理使用本地存储可显著提升应用性能和用户体验。`
              }
            ]
          },
          {
            question: `23.大文件上传如何做断点续传？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `断点续传是大文件上传的核心需求，通过记录已上传片段，支持从中断处继续上传。以下是实现方案的深度解析：
一、核心原理
文件分片：将大文件分割为固定大小的小块（如 1MB / 片）。
唯一标识：为文件生成唯一哈希（如 MD5），用于标识文件。
已上传记录：记录已上传的分片，断点续传时跳过已上传部分。
并发上传：多线程并行上传多个分片，提升效率。
服务端合并：所有分片上传完成后，服务端按顺序合并文件。
二、前端实现步骤
1. 文件分片
使用 File.slice() 方法分割文件：
javascript
function createChunks(file, chunkSize = 1024 * 1024) { // 1MB/片
  const chunks = [];
  let offset = 0;
  
  while (offset < file.size) {
    chunks.push(file.slice(offset, offset + chunkSize));
    offset += chunkSize;
  }
  
  return chunks;
}
2. 生成文件哈希
使用 SparkMD5 库计算文件哈希（异步计算，避免阻塞主线程）：
javascript
async function calculateFileHash(file) {
  return new Promise((resolve) => {
    const chunkSize = 2 * 1024 * 1024; // 2MB/块
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    let offset = 0;
    
    const processChunk = () => {
      const slice = file.slice(offset, offset + chunkSize);
      fileReader.readAsArrayBuffer(slice);
    };
    
    fileReader.onload = (e) => {
      spark.append(e.target.result);
      offset += chunkSize;
      
      if (offset < file.size) {
        processChunk();
      } else {
        resolve(spark.end()); // 返回文件哈希
      }
    };
    
    processChunk();
  });
}
3. 检查已上传分片
javascript
async function checkUploadedChunks(fileHash) {
  const response = await fetch('/ api / check ? fileHash = ￥{ fileHash }');
  return response.json(); // 返回已上传的分片列表
}
4. 并发上传分片
使用 Promise.all 控制并发：
javascript
async function uploadChunks(file, fileHash, uploadedChunks = []) {
  const chunks = createChunks(file);
  const uploadPromises = [];
  const concurrency = 3; // 并发数
  
  // 过滤掉已上传的分片
  const pendingChunks = chunks.filter((_, index) => !uploadedChunks.includes(index));
  
  // 分批上传
  for (let i = 0; i < pendingChunks.length; i += concurrency) {
    const batch = pendingChunks.slice(i, i + concurrency);
    
    const batchPromises = batch.map((chunk, batchIndex) => {
      const chunkIndex = uploadedChunks.length + i + batchIndex;
      const formData = new FormData();
      
      formData.append('file', chunk);
      formData.append('fileHash', fileHash);
      formData.append('chunkIndex', chunkIndex);
      formData.append('totalChunks', chunks.length);
      
      return fetch('/api/upload', { method: 'POST', body: formData });
    });
    
    await Promise.all(batchPromises);
  }
  
  // 通知服务端合并文件
  return fetch('/api/merge', {
    method: 'POST',
    body: JSON.stringify({ fileHash, totalChunks: chunks.length })
  });
}
三、服务端实现要点
1. 检查接口
javascript
// 检查已上传的分片
app.get('/api/check', async (req, res) => {
  const { fileHash } = req.query;
  const uploadedChunks = await listFilesInDirectory('temp / ￥{ fileHash }');
  res.json(uploadedChunks.map(file => parseInt(file.split('.')[0])));
});
2. 上传接口
javascript
// 接收分片
app.post('/api/upload', async (req, res) => {
  const { fileHash, chunkIndex, totalChunks } = req.body;
  const file = req.files.file; // 使用 multer 中间件处理文件
  
  // 保存分片到临时目录
  const tempDir = 'temp / ￥{ fileHash }';
  await mkdir(tempDir, { recursive: true });
  await move(file.path, '￥{ tempDir } / ￥{ chunkIndex }.part');
  
  res.status(200).send();
});
3. 合并接口
javascript
// 合并分片
app.post('/api/merge', async (req, res) => {
  const { fileHash, totalChunks } = req.body;
  const tempDir = 'temp / ￥{ fileHash }';
  const outputPath = 'uploads / ￥{ fileHash }.￥{ getOriginalExtension(fileHash) }';
  
  // 按顺序读取所有分片
  const chunks = await Promise.all(
    Array.from({ length: totalChunks }, (_, i) => readFile('￥{ tempDir } / ￥{ i }.part'))
  );
  
  // 合并文件
  await writeFile(outputPath, Buffer.concat(chunks));
  
  // 清理临时文件
  await rm(tempDir, { recursive: true });
  
  res.status(200).json({ path: outputPath });
});
四、断点续传的关键细节
进度记录
前端：使用 localStorage 记录已上传分片。
服务端：保存已上传分片的元数据。
错误处理
重试机制：上传失败的分片自动重试。
超时控制：设置合理的超时时间，避免长时间等待。
文件完整性校验
上传前和合并后分别计算文件哈希，确保一致性。
并发控制
限制并发数（如 3-5 个），避免过多请求导致网络拥塞。
五、常见面试问题
1. 如何生成文件唯一标识？
文件哈希：计算整个文件的 MD5/SHA-1。
优化方案：取文件前中后各部分计算哈希，减少计算量。
2. 如何处理分片上传顺序？
服务端校验：按分片索引重新排序后合并。
依赖管理：使用队列确保前一个分片成功后再上传下一个。
3. 如何实现秒传？
上传前计算文件哈希，查询服务端是否存在相同哈希的文件。
若存在，直接返回文件路径，无需实际上传。
4. 大文件上传的性能优化？
并发上传：多线程并行上传分片。
压缩传输：使用 gzip 压缩分片数据。
预加载：提前计算文件哈希，减少等待时间。
六、总结
断点续传的核心流程：
前端：分片 → 计算哈希 → 检查已上传分片 → 并发上传剩余分片。
服务端：接收分片 → 保存临时文件 → 合并文件 → 校验完整性。
关键技术点：
文件分片与哈希计算。
并发控制与错误重试。
服务端文件合并与校验。
实际项目中可使用现有库（如 tus-js-client、plupload）简化开发，但需理解核心原理以应对复杂场景。`
              }
            ]
          },
          {
            question: `25.什么是防抖和节流？有什么区别？如何实现?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `防抖（Debounce）和节流（Throttle）是优化高频触发事件的两种技术，常见于滚动监听、搜索联想、按钮点击等场景。以下是核心区别和实现方案：
一、核心概念
1. 防抖（Debounce）
原理：在事件被触发后，延迟 n 毫秒再执行回调；若在这 n 毫秒内事件再次被触发，则重新计时。
效果：将多次触发合并为一次执行。
适用场景：搜索联想、窗口 resize、文本输入校验等。
2. 节流（Throttle）
原理：在一定时间内，只执行一次回调函数。
效果：将高频触发限制为按固定频率执行。
适用场景：滚动加载、按钮防连击、游戏技能冷却等。
二、区别对比
对比项	防抖（Debounce）	节流（Throttle）
触发频率	仅最后一次有效	固定频率执行
执行时机	延迟执行	立即执行（或延迟执行）
应用场景	避免频繁触发（如搜索联想）	控制执行频率（如滚动加载）
实现关键	清除上一次定时器	记录上次执行时间
三、实现方案
1. 防抖实现
javascript
function debounce(func, wait, immediate = false) {
  let timer = null;
  
  return function(...args) {
    const context = this;
    
    // 清除上一次定时器
    if (timer) clearTimeout(timer);
    
    if (immediate) {
      // 立即执行版本
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      
      if (callNow) func.apply(context, args);
    } else {
      // 延迟执行版本
      timer = setTimeout(() => {
        func.apply(context, args);
      }, wait);
    }
  };
}

// 使用示例
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
  console.log('搜索:', e.target.value);
}, 300));
2. 节流实现
javascript
// 时间戳版（立即执行）
function throttle(func, limit) {
  let lastExecTime = 0;
  
  return function(...args) {
    const context = this;
    const now = Date.now();
    
    if (now - lastExecTime >= limit) {
      func.apply(context, args);
      lastExecTime = now;
    }
  };
}

// 定时器版（延迟执行）
function throttle(func, limit) {
  let timer = null;
  
  return function(...args) {
    const context = this;
    
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(context, args);
        timer = null;
      }, limit);
    }
  };
}

// 结合版（兼顾立即执行和延迟执行）
function throttle(func, limit) {
  let timer = null;
  let lastExecTime = 0;
  
  return function(...args) {
    const context = this;
    const now = Date.now();
    const remaining = limit - (now - lastExecTime);
    
    // 清除过期定时器
    if (timer && remaining <= 0) {
      clearTimeout(timer);
      timer = null;
    }
    
    if (!timer) {
      func.apply(context, args);
      lastExecTime = now;
      
      timer = setTimeout(() => {
        timer = null;
      }, limit);
    }
  };
}

// 使用示例
window.addEventListener('scroll', throttle(() => {
  console.log('滚动事件触发');
}, 200));
四、常见面试问题
1. 手写防抖函数
关键点：
使用闭包保存定时器。
每次触发时清除上一次定时器。
支持立即执行选项。
2. 手写节流函数
关键点：
记录上次执行时间或使用定时器。
控制执行频率。
支持立即执行和延迟执行。
3. 防抖和节流的适用场景
防抖：搜索框联想、窗口 resize、按钮重复提交。
节流：滚动加载、高频点击（如点赞）、游戏技能冷却。
4. 如何优化防抖和节流？
取消功能：添加取消方法，用于提前清除定时器。
leading/trailing 选项：控制首次或末次是否执行。
上下文绑定：确保回调函数中的 this 指向正确。
五、总结
防抖：将多次触发合并为一次，适合需要避免频繁执行的场景。
节流：控制执行频率，适合需要固定频率执行的场景。
实现核心：防抖依赖定时器重置，节流依赖时间戳或定时器限制。
在实际应用中，可根据具体需求选择或组合使用这两种技术，提升应用性能和用户体验。`
              }
            ]
          },
          {
            question: `26.如何判断一个元素是否在可视区域中？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `判断元素是否在可视区域（Viewport）是前端开发中的常见需求，常用于实现懒加载、滚动动画、曝光统计等功能。以下是几种主流方案及其对比：
一、传统方案：getBoundingClientRect()
1. 核心原理
通过元素的 getBoundingClientRect() 方法获取其位置信息，再与视口尺寸比较：
javascript
function isInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}
2. 扩展：部分可见判断
若允许元素部分可见，可调整判断条件：
javascript
function isPartiallyInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.bottom >= 0 &&
    rect.top <= (window.innerHeight || document.documentElement.clientHeight)
  );
}
3. 应用场景
图片懒加载
滚动时的元素动画触发
曝光统计
二、高性能方案：Intersection Observer API
1. 核心原理
使用浏览器原生的 IntersectionObserver 监听元素与视口的交叉状态：
javascript
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // 元素进入视口
        console.log('元素可见');
        // 可选择性停止监听
        observer.unobserve(entry.target);
      } else {
        // 元素离开视口
        console.log('元素不可见');
      }
    });
  },
  {
    root: null, // 默认为视口
    rootMargin: '0px', // 扩展视口边界
    threshold: 0.1 // 元素可见比例达到10%时触发
  }
);

// 开始监听元素
observer.observe(document.getElementById('target'));
2. 关键参数
root：指定监听的父元素（默认为视口）。
rootMargin：扩展 / 收缩视口边界（如 '-10px 0px'）。
threshold：触发回调的交叉比例（0-1）。
3. 优势
浏览器原生支持，性能优化（异步监听，不阻塞渲染）。
避免频繁计算 DOM 位置（如滚动时）。
支持复杂配置（如阈值、根元素）。
三、兼容性方案：结合 scroll 事件
1. 实现思路
监听窗口滚动事件，在回调中计算元素位置：
javascript
function handleScroll() {
  const element = document.getElementById('target');
  if (isInViewport(element)) {
    console.log('元素可见');
  }
}

window.addEventListener('scroll', handleScroll);
2. 性能优化
使用防抖（Debounce）或节流（Throttle）减少计算频率：
javascript
window.addEventListener('scroll', throttle(handleScroll, 200));
四、各方案对比
方案	优点	缺点	适用场景
getBoundingClientRect()	兼容性好	手动计算，频繁调用影响性能	简单场景，兼容性要求高
Intersection Observer	高性能，异步监听	浏览器兼容性（需 polyfill）	复杂场景，性能要求高
scroll 事件 + 节流	完全可控	需手动处理节流，仍有性能开销	需精确控制触发时机
五、常见面试问题
1. 如何优化元素可见性检测？
使用 Intersection Observer 替代手动计算。
对 scroll 事件做节流处理。
批量处理元素（如懒加载图片时）。
2. Intersection Observer 的 threshold 有什么用？
用于设置元素可见比例的触发阈值。例如：
threshold: 0：元素一进入视口即触发。
threshold: 0.5：元素一半可见时触发。
threshold: [0, 0.5, 1]：分别在 0%、50%、100% 可见时触发。
3. 实现图片懒加载的完整流程
将图片 src 替换为占位符（如灰色图）。
使用 data-src 存储真实图片地址。
监听图片元素是否进入视口（Intersection Observer）。
元素可见时，将 data-src 值赋给 src。
javascript
// 示例代码
const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

images.forEach((img) => observer.observe(img));


六、总结
优先使用 Intersection Observer：现代浏览器支持，性能最优。
兼容性要求高时：使用 getBoundingClientRect() + 节流。
精确控制触发时机：结合 scroll 事件手动计算。
掌握这些方案可以高效解决元素可见性检测问题，提升应用性能和用户体验。`
              }
            ]
          },
          {
            question: `27.什么是单点登录?如何实现?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `什么是单点登录？
单点登录（Single Sign-On，简称 SSO）是一种身份认证机制，允许用户在多个相互信任的应用系统中，只需登录一次，即可访问所有关联系统，无需重复输入账号密码。
例如：登录支付宝后，访问淘宝、闲鱼等阿里系应用无需再次登录，这就是典型的单点登录场景。
核心设计目标
用户体验：减少重复登录操作，提升使用便捷性。
安全性：统一身份认证入口，便于权限管理和风险控制。
系统集成：解决多系统间的身份互通问题。
实现单点登录的核心思路
单点登录的本质是 多系统间共享用户的登录状态，关键在于如何安全地传递和验证登录凭证。以下是几种主流实现方案：
一、基于 Cookie + 共享 Session 的方案（适用于同域名下的多系统）
原理：
多个应用部署在 同一主域名 下（如 app1.example.com、app2.example.com）。
用户在任意应用登录后，服务器将登录状态（Session）存储在后端，同时向客户端写入 跨子域的 Cookie（设置 domain=.example.com）。
其他子域应用读取该 Cookie 中的 Session ID，通过后端验证 Session 有效性，实现登录状态共享。
实现步骤：
登录成功后，服务器设置 Cookie：Set-Cookie: sso_session=xxx; domain=.example.com; path=/。
其他子域应用请求时自动携带该 Cookie，后端通过 sso_session 查询 Session 验证身份。
优缺点：
优点：实现简单，适合同域系统。
缺点：
仅限同主域名下的系统，无法跨不同域名。
Session 存储在服务器，分布式系统需考虑 Session 共享（如使用 Redis 集中存储）。
二、基于 Token 的方案（适用于跨域名多系统）
原理：
引入 统一认证中心（SSO 服务器），所有应用的登录请求都跳转至该中心处理。
用户登录成功后，SSO 服务器生成 加密 Token（包含用户身份信息、有效期等），返回给客户端。
客户端访问其他应用时，携带该 Token，应用服务器向 SSO 服务器验证 Token 有效性，确认身份后允许访问。
实现步骤：
步骤 1：用户访问应用 A，未登录则跳转至 SSO 服务器（https://sso.example.com/login）。
步骤 2：用户在 SSO 服务器输入账号密码，验证通过后生成 Token（如 JWT 格式）。
步骤 3：SSO 服务器将 Token 作为参数，跳转回应用 A（https://app1.example.com?token=xxx）。
步骤 4：应用 A 存储 Token（如 localStorage），并在后续请求中通过 Header 携带 Token。
步骤 5：应用 A 服务器收到请求后，向 SSO 服务器发送 Token 验证请求（https://sso.example.com/verify?token=xxx）。
步骤 6：SSO 服务器验证 Token 有效，返回用户信息，应用 A 允许访问。
步骤 7：用户访问应用 B 时，应用 B 检测到未登录，跳转至 SSO 服务器，SSO 服务器发现用户已登录，直接生成 Token 并跳转回应用 B。
关键技术：
Token 格式：常用 JWT（JSON Web Token），包含 Header（加密算法）、Payload（用户信息）、Signature（签名），可自验证无需查询服务器。
Token 存储：客户端存储在 localStorage 或 sessionStorage，需注意 XSS 攻击风险。
Token 传递：通过 URL 参数、请求头（如 Authorization: Bearer xxx）传递。
优缺点：
优点：支持跨域名、无状态（服务器无需存储 Session）、适合分布式系统。
缺点：
Token 一旦生成无法轻易作废（需结合黑名单机制处理注销）。
需确保 Token 传输和存储的安全性（如使用 HTTPS、避免明文存储敏感信息）。
三、基于 SAML/OAuth2.0 的方案（适用于第三方系统集成）
SAML（安全断言标记语言）：
适用于企业级单点登录（如企业内部系统与第三方服务集成）。
通过 XML 格式的断言（Assertion）传递用户身份信息，安全性高，但实现复杂。
OAuth2.0 + OpenID Connect（OIDC）：
OAuth2.0 原本用于授权（如第三方应用获取用户信息的权限），结合 OIDC 可实现身份认证。
流程：用户通过第三方平台（如微信、GitHub）登录，应用获取 ID Token（包含用户身份），实现单点登录。
典型场景：网站的 “微信登录”“QQ 登录” 功能。
单点登录的关键问题及解决方案
注销登录：
需实现 全局注销：用户在一个应用注销时，通知 SSO 服务器，SSO 服务器向所有关联应用发送注销请求，清除本地 Token 或 Session。
安全性保障：
使用 HTTPS 加密传输，防止 Token 被窃听。
对 Token 设置 短期有效期，结合刷新 Token（Refresh Token）机制延长登录状态。
防止 CSRF 攻击：验证请求的 Referer 或使用 CSRF Token。
防止 XSS 攻击：避免在 localStorage 存储敏感信息，对输入进行过滤。
跨域问题：
Token 存储在 localStorage 时，可通过 iframe + postMessage 实现跨域通信（如 SSO 服务器通知其他应用更新登录状态）。
总结
同域多系统：优先使用 Cookie + 共享 Session 方案，简单高效。
跨域多系统：采用 Token 方案（如 JWT），结合统一认证中心。
第三方登录集成：使用 OAuth2.0 + OIDC 方案，对接成熟平台。
单点登录的核心是通过统一的认证中心和安全的凭证（Session/Token），实现多系统间的身份共享，平衡用户体验与系统安全性。`
              }
            ]
          },
          {
            question: `28.如何实现上拉加载，下拉刷新?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `上拉加载与下拉刷新的实现原理
上拉加载（滚动到底部自动加载更多数据）和下拉刷新（下拉页面触发数据刷新）是移动端应用常见的交互模式。以下是核心实现方案：
一、下拉刷新（Pull to Refresh）
核心原理
监听触摸事件：捕获用户下拉动作（touchstart、touchmove、touchend）。
计算位移：判断下拉距离是否超过阈值（如 50px）。
状态切换：根据下拉距离切换不同状态（正常 → 准备刷新 → 刷新中）。
执行刷新：松手时若达到阈值，触发刷新逻辑。`
              },
              {
                text: `实现代码（原生 JavaScript）`,
                daima: `const container = document.querySelector('.refresh-container');
const refreshIcon = document.querySelector('.refresh-icon');
const refreshText = document.querySelector('.refresh-text');

let startY = 0;
let currentY = 0;
let isRefreshing = false;
const threshold = 60; // 触发刷新的阈值

container.addEventListener('touchstart', (e) => {
  if (isRefreshing || window.scrollY > 0) return;
  startY = e.touches[0].clientY;
});

container.addEventListener('touchmove', (e) => {
  if (isRefreshing || window.scrollY > 0) return;
  currentY = e.touches[0].clientY;
  const diff = currentY - startY;
  
  if (diff > 0) {
    e.preventDefault(); // 阻止页面滚动
    container.style.transform = 'translateY(￥{ diff/ 2}px)'; // 阻尼效果
    
    if (diff > threshold) {
      refreshText.textContent = '释放刷新';
      refreshIcon.style.transform = 'rotate(180deg)';
    } else {
      refreshText.textContent = '下拉刷新';
      refreshIcon.style.transform = 'rotate(0)';
    }
  }
});

container.addEventListener('touchend', () => {
  if (isRefreshing) return;
  
  const diff = currentY - startY;
  if (diff > threshold) {
    // 触发刷新
    isRefreshing = true;
    container.style.transform = 'translateY(￥{ threshold/ 2}px)';
    refreshText.textContent = '刷新中...';
    refreshIcon.classList.add('refreshing');
    
    // 模拟刷新操作
    setTimeout(() => {
      // 刷新完成
      isRefreshing = false;
      container.style.transform = 'translateY(0)';
      refreshText.textContent = '刷新完成';
      refreshIcon.classList.remove('refreshing');
      
      // 刷新完成提示短暂显示后恢复
      setTimeout(() => {
        refreshText.textContent = '下拉刷新';
      }, 1000);
    }, 1500);
  } else {
    // 未达到阈值，恢复原状
    container.style.transform = 'translateY(0)';
  }
  
  startY = 0;
  currentY = 0;
});
关键优化点
阻尼效果：下拉距离越大，阻力越大（通过 diff/2 实现）。
状态管理：明确区分 “下拉中”“准备刷新”“刷新中” 等状态。
防止冲突：仅在页面顶部时触发下拉刷新（window.scrollY === 0）。
二、上拉加载（Infinite Scroll）
核心原理
监听滚动事件：检测页面是否滚动到底部。
触发条件：当滚动条距离底部小于某个阈值时（如 100px）。
加载数据：触发加载更多数据的请求，并在数据返回后追加到页面。
状态控制：避免重复加载，显示加载状态提示。
`,
              },
              {
                text: `实现代码（原生 JavaScript）`,
                daima: `const container = document.querySelector('.load-more-container');
const loadingIndicator = document.querySelector('.loading-indicator');
let isLoading = false;
let page = 1;

// 监听滚动事件
window.addEventListener('scroll', handleScroll);

function handleScroll() {
  // 避免重复加载
  if (isLoading) return;
  
  // 计算滚动条是否接近底部
  const scrollHeight = document.documentElement.scrollHeight;
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  const clientHeight = document.documentElement.clientHeight;
  
  // 当距离底部 100px 时触发加载
  if (scrollTop + clientHeight >= scrollHeight - 100) {
    loadMoreData();
  }
}

async function loadMoreData() {
  isLoading = true;
  loadingIndicator.style.display = 'block';
  
  try {
    // 模拟请求数据
    const response = await fetch('/ api / data ? page = ￥{ page + 1}'');
    const newData = await response.json();
    
    if (newData.length === 0) {
      // 没有更多数据
      loadingIndicator.textContent = '没有更多数据';
      return;
    }
    
    page++;
    appendData(newData);
  } catch (error) {
    console.error('加载失败:', error);
    loadingIndicator.textContent = '加载失败，点击重试';
    loadingIndicator.addEventListener('click', () => {
      loadingIndicator.textContent = '加载中...';
      loadMoreData();
    });
  } finally {
    isLoading = false;
    // 隐藏加载指示器（如果没有更多数据则保持显示）
    if (loadingIndicator.textContent !== '没有更多数据') {
      loadingIndicator.style.display = 'none';
    }
  }
}

function appendData(data) {
  // 将新数据追加到容器中
  const content = document.querySelector('.content');
  data.forEach(item => {
    const itemElement = document.createElement('div');
    itemElement.className = 'item';
    itemElement.textContent = item.title;
    content.appendChild(itemElement);
  });
}

关键优化点
节流处理：使用节流（Throttle）避免频繁触发滚动事件。
加载状态：显示明确的加载中提示，防止重复请求。
边界处理：检查是否还有更多数据，避免无限请求。
性能优化：使用 requestAnimationFrame 优化滚动监听。`,
              },
              {
                text: `三、框架实现方案
1. React 实现（使用 hooks）`,
                daima: `import { useState, useEffect } from 'react';

function InfiniteScrollComponent() {
  const [data, setData] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  // 加载数据
  const loadMore = async () => {
    if (loading || !hasMore) return;
    
    setLoading(true);
    try {
      const response = await fetch('/ api / data ? page = ￥{ page }');
      const newData = await response.json();
      
      setData(prevData => [...prevData, ...newData]);
      setPage(prevPage => prevPage + 1);
      setHasMore(newData.length > 0);
    } catch (error) {
      console.error('加载失败', error);
    } finally {
      setLoading(false);
    }
  };

  // 监听滚动
  useEffect(() => {
    const handleScroll = () => {
      if (window.innerHeight + document.documentElement.scrollTop 
          >= document.documentElement.offsetHeight - 100) {
        loadMore();
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [loading, hasMore, loadMore]);

  // 初始加载
  useEffect(() => {
    loadMore();
  }, []);

  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.content}</div>
      ))}
      {loading && <div>加载中...</div>}
      {!hasMore && <div>没有更多数据</div>}
    </div>
  );
}
四、性能优化建议
虚拟列表（Virtual List）：
当数据量极大时，只渲染可视区域内的元素，显著提升性能。
推荐库：React 可用 react-window，Vue 可用 vue-virtual-scroller。
节流与防抖：
对滚动事件使用节流，避免频繁触发回调。
示例：`,
              },
              {
                text: ``,
                daima: `function throttle(fn, delay) {
  let timer = null;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}

window.addEventListener('scroll', throttle(handleScroll, 200));
骨架屏（Skeleton Screen）：
加载过程中显示骨架屏占位，提升用户体验。
五、常见面试问题
如何判断滚动到底部？`,
              },
              {
                text: ``,
                daima: `const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight;

// 距离底部 100px 时触发
if (scrollTop + clientHeight >= scrollHeight - 100) {
  // 加载更多
}
下拉刷新时如何防止页面滚动？
在下拉过程中调用 e.preventDefault() 阻止默认滚动行为。
注意：仅在页面顶部且下拉距离大于 0 时阻止。
如何处理上拉加载的边界情况（如没有更多数据）？
添加 hasMore 状态标记是否还有更多数据。
无更多数据时显示明确提示，停止监听滚动事件。
上拉加载和下拉刷新的性能优化点？
节流处理滚动事件。
使用虚拟列表渲染大量数据。
图片懒加载。
减少 DOM 操作，使用 DocumentFragment 批量插入元素。
六、总结
下拉刷新：通过监听触摸事件，计算位移并切换状态，达到阈值时触发刷新。
上拉加载：监听滚动事件，判断是否接近底部，触发数据加载。
框架实现：利用状态管理简化逻辑，结合生命周期钩子监听事件。
性能优化：节流、虚拟列表、骨架屏是关键。
掌握这些技术点可以实现流畅的上拉加载和下拉刷新体验，提升移动端应用的用户体验。`,
              }
            ]
          },
          {
            question: `30.说说你对函数式编程的理解？优缺点?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `函数式编程（Functional Programming，简称 FP）是一种编程范式，它将计算视为函数的求值，避免使用共享状态和可变数据。以下是其核心概念、优缺点及应用场景的深度解析：
一、核心概念
1. 纯函数（Pure Functions）
特性：
相同输入，永远返回相同输出，不依赖外部状态。
无副作用（No Side Effects）：不修改外部数据、不触发 API 调用等。
示例：
javascript
// 纯函数
function add(a, b) {
  return a + b;
}

// 非纯函数（依赖外部变量）
let counter = 0;
function increment() {
  return ++counter;
}

2. 不可变数据（Immutability）
数据一旦创建就不能被修改，如需修改则返回新数据。
JavaScript 中的实现：
javascript
const arr = [1, 2, 3];

// 错误：修改原数组
arr.push(4);

// 正确：创建新数组
const newArr = [...arr, 4];

3. 高阶函数（Higher-Order Functions）
定义：接收函数作为参数或返回函数的函数。
常见示例：map、filter、reduce。
javascript
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

4. 函数组合（Function Composition）
将多个简单函数组合成复杂函数。
示例：
javascript
const compose = (f, g) => x => f(g(x));

const add1 = x => x + 1;
const double = x => x * 2;

const add1ThenDouble = compose(double, add1);
add1ThenDouble(3); // (3+1)*2 = 8

5. 避免共享状态和副作用
共享状态会导致代码复杂性和不可预测性。
函数式编程通过纯函数和不可变数据消除这些问题。
二、优缺点对比
优点
代码简洁，可读性高
示例：计算数组中所有偶数的平方和
javascript
// 函数式
const sum = numbers
  .filter(x => x % 2 === 0)
  .map(x => x * x)
  .reduce((acc, x) => acc + x, 0);

// 命令式
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    sum += numbers[i] * numbers[i];
  }
}

可维护性强
纯函数不依赖外部状态，易于测试和调试。
易于并行 / 并发
无副作用的纯函数无需考虑线程安全问题，适合并行计算。
可缓存性
相同输入总是返回相同输出，可实现自动缓存（Memoization）。
javascript
function memoize(fn) {
  const cache = new Map();
  return x => {
    if (cache.has(x)) return cache.get(x);
    const result = fn(x);
    cache.set(x, result);
    return result;
  };
}

强大的抽象能力
通过高阶函数和组合，可构建复杂的抽象工具（如 RxJS 的数据流处理）。
缺点
学习曲线陡峭
需要理解闭包、高阶函数、范畴论等概念。
性能开销
频繁创建新对象（如不可变数据）可能影响性能。
代码可读性的双刃剑
过度抽象可能导致代码晦涩难懂（如嵌套过深的函数组合）。
不适合所有场景
例如，需要频繁修改状态的场景（如游戏开发）使用命令式编程更高效。
三、常见应用场景
数据处理与转换
使用 map、filter、reduce 处理集合数据。
异步编程
使用 Promise、Async/Await 或 RxJS 处理异步数据流。
状态管理
如 Redux，通过纯函数（reducers）管理不可变状态。
函数式库
Lodash/FP：函数式版本的 Lodash。
Ramda：专注于不可变数据和函数组合。
RxJS：响应式编程，基于函数式理念处理数据流。
四、与面向对象编程（OOP）的对比
维度	函数式编程（FP）	面向对象编程（OOP）
核心思想	将计算视为函数求值	以对象为中心，通过消息传递交互
数据处理	不可变数据，纯函数	可变数据，对象方法修改状态
代码结构	函数组合，避免共享状态	继承、多态，依赖对象状态
副作用	尽量避免	常见（如修改对象属性）
代表语言	Haskell、Lisp、JavaScript	Java、Python、C++
五、常见面试问题
1. 什么是纯函数？为什么重要？
定义：相同输入始终返回相同输出，无副作用。
重要性：可测试性、可缓存性、易于并行化。
2. 如何实现函数式编程中的不可变数据？
使用 JavaScript 的扩展运算符（...）。
使用 Object.assign() 创建新对象。
使用库如 Immutable.js 或 Immer。
3. 说说你对函数组合的理解
函数组合是将多个函数组合成一个新函数的过程。
数学表示：(f ∘ g)(x) = f(g(x))。
示例：
javascript
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

4. 函数式编程在前端中的应用
React 的纯组件和 Hooks。
Redux 的 reducer 纯函数。
RxJS 处理异步事件流。
使用 map/filter/reduce 处理 UI 数据。
六、总结
函数式编程通过纯函数、不可变数据和高阶函数，提供了一种更简洁、更可靠的编程范式，特别适合处理数据转换、异步流和复杂抽象。但其学习成本和性能开销需在实际项目中权衡。在前端领域，函数式编程已成为主流趋势，掌握其核心思想对提升代码质量和可维护性至关重要。`
              }
            ]
          },
          {
            question: `31.web常见的攻击方式有哪些？如何防御?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Web 常见攻击方式及防御措施
Web 安全是前端开发的核心关注点之一。以下是常见的 Web 攻击方式及其防御方法：
一、XSS 攻击（跨站脚本攻击）
原理：攻击者通过注入恶意脚本到目标网站，当用户访问时执行脚本获取敏感信息。
类型：
反射型 XSS：恶意脚本作为 URL 参数传递，服务器未过滤直接返回。
示例：http://example.com/search?keyword=《 script>alert('XSS')<script />
存储型 XSS：恶意脚本存储在服务器数据库（如评论、留言），所有访问者都会执行。
DOM 型 XSS：通过修改页面 DOM 结构注入脚本（纯前端攻击）。
防御措施：
输入过滤：对用户输入进行严格过滤，禁止特殊字符（如 <,>）。
  输出编码：对动态输出到页面的内容进行 HTML 编码。
  function escapeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

CSP（内容安全政策）：通过 HTTP 头限制页面可加载的资源来源。
http
Content-Security-Policy: default-src 'self'; script-src 'self' trusted.example.com

HttpOnly Cookie：禁止 JavaScript 访问敏感 Cookie。
二、CSRF 攻击（跨站请求伪造）
原理：攻击者诱导用户在已登录的网站执行恶意操作，利用用户的身份凭证。
示例：
用户已登录银行网站（bank.com）。
攻击者诱导用户访问恶意网站（malicious.com），该网站向 bank.com 发送转账请求。
浏览器自动携带 bank.com 的 Cookie，导致请求被服务器接受。
防御措施：
SameSite Cookie：设置 Cookie 的 SameSite 属性，限制跨站请求时 Cookie 的发送。
http
Set-Cookie: session_id=123; SameSite=Strict

CSRF Token：
服务器生成随机 Token 并嵌入页面。
客户端在请求时携带该 Token，服务器验证一致性。
验证请求来源：检查 HTTP 头的 Origin 或 Referer 字段，确保请求来自合法域名。
三、SQL 注入攻击
原理：攻击者通过构造恶意 SQL 参数，篡改数据库查询逻辑。
示例：
登录表单的 SQL 查询：SELECT * FROM users WHERE username='$username' AND password='$password'
攻击者输入用户名：' OR '1'='1，导致 SQL 变为：
sql
SELECT * FROM users WHERE username='' OR '1'='1' AND password=''

该条件永远为真，攻击者可绕过验证。
防御措施：
参数化查询：使用预编译语句（Prepared Statements），避免 SQL 拼接。
javascript
// Node.js + MySQL 示例
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(query, [username, password], (err, results) => { ... });

输入验证：限制输入长度和类型，禁止特殊字符。
最小权限原则：数据库用户仅拥有必要的最低权限。
四、点击劫持（Clickjacking）
原理：攻击者通过透明 iframe 覆盖目标网站，诱导用户在不知情的情况下点击。
防御措施：
X-Frame-Options 头：限制页面能否被其他网站通过 iframe 加载。
http
X-Frame-Options: DENY  // 禁止任何网站嵌入
X-Frame-Options: SAMEORIGIN  // 仅允许同源网站嵌入

CSP 指令：使用 frame-ancestors 限制 iframe 嵌入。
http
Content-Security-Policy: frame-ancestors 'none';

JavaScript 防御：检测页面是否被嵌套，若是则自动跳转。
javascript
if (window.top !== window.self) {
  window.top.location = window.self.location;
}

五、DDOS 攻击（分布式拒绝服务）
原理：攻击者通过大量傀儡机器（僵尸网络）向目标服务器发送请求，耗尽资源使其无法响应正常用户。
防御措施：
流量过滤：使用 CDN 或专业防护服务（如 Cloudflare）过滤异常流量。
限流策略：服务器设置请求频率限制（如每分钟 100 次请求）。
负载均衡：分散流量到多个服务器。
验证码：对敏感操作要求用户完成验证码，过滤自动化攻击。
六、中间人攻击（MITM）
原理：攻击者拦截并篡改通信双方的信息，通常通过伪造证书或网络嗅探。
防御措施：
使用 HTTPS：通过 SSL/TLS 加密通信，验证服务器证书。
HSTS（HTTP 严格传输安全）：强制浏览器只能通过 HTTPS 访问网站。
http
Strict-Transport-Security: max-age=31536000; includeSubDomains

证书验证：客户端验证服务器证书的有效性，防止伪造证书。
七、URL 重定向漏洞
原理：攻击者通过构造恶意重定向 URL，诱导用户访问钓鱼网站。
示例：
plaintext
http://example.com/redirect?url=http://malicious.com
防御措施：
白名单验证：仅允许重定向到预定义的合法域名。
显式确认：对重定向操作提示用户确认，避免自动跳转。
八、总结
攻击类型	核心原理	关键防御措施
XSS	注入恶意脚本	输入过滤、输出编码、CSP、HttpOnly Cookie
CSRF	利用用户身份执行恶意请求	SameSite Cookie、CSRF Token、验证来源
SQL 注入	篡改数据库查询逻辑	参数化查询、输入验证
点击劫持	透明 iframe 诱导点击	X-Frame-Options、CSP
DDOS	耗尽服务器资源	流量过滤、限流、负载均衡
中间人攻击	拦截并篡改通信	HTTPS、HSTS、证书验证
URL 重定向漏洞	诱导用户访问恶意网站	白名单验证、显式确认
最佳实践：
永远不要信任用户输入，对所有外部输入进行严格验证和过滤。
最小化敏感信息的暴露（如使用 HttpOnly Cookie 存储会话 ID）。
定期更新依赖库和框架，修复已知安全漏洞。
使用安全头（如 CSP、HSTS）增强防护。
对敏感操作实施多因素认证。`
              }
            ]
          },
          {
            question: `32.说说JavaScript中内存泄漏的几种情况?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，内存泄漏指的是程序中已不再使用的内存无法被垃圾回收机制释放的情况。以下是几种常见的内存泄漏场景：
1. 全局变量导致的内存泄漏
原因：在函数内部未声明直接赋值的变量，或通过 this、window 等全局对象创建的变量，会成为全局变量。全局变量不会被垃圾回收，若持续占用大量内存，可能导致泄漏。
示例：
javascript
function leak() {
  // 未声明的变量会成为全局变量
  leakedVar = 'This will leak!';
  // 等价于 window.leakedVar = 'This will leak!';
}

2. 未清理的定时器和回调函数
原因：setInterval、setTimeout 等定时器的回调函数及其中引用的变量，在定时器未被清除时不会被回收。若定时器持续运行且引用了大对象（如 DOM 元素），会导致内存泄漏。
示例：
javascript
const element = document.getElementById('leaky-element');
const interval = setInterval(() => {
  console.log(element.textContent); // 定时器持续引用 element
}, 1000);
// 若未调用 clearInterval(interval)，即使 element 被移除，内存也不会释放

3. 闭包导致的内存泄漏
原因：闭包会捕获并持有其外部函数的变量引用，若闭包长期存在（如作为事件监听器），其引用的变量无法被回收。
示例：
javascript
function createLeak() {
  const largeData = new Array(1000).fill('data');
  return function() {
    console.log(largeData.length); // 闭包引用 largeData
  };
}
const leakyFunc = createLeak(); // leakyFunc 持续引用 largeData

4. DOM 引用未释放
原因：若 JavaScript 中保留了对 DOM 元素的引用，即使该元素在 DOM 中被移除，内存也不会释放。若存在多个引用路径（如缓存、事件监听器），需确保所有引用都被清除。
示例：
javascript
const elements = [];
const element = document.getElementById('to-remove');
elements.push(element); // 数组引用了元素
document.body.removeChild(element); // DOM中移除元素，但 elements 仍保留引用

5. 未移除的事件监听器
原因：为 DOM 元素添加的事件监听器若未在元素被移除前手动清除，会导致内存泄漏。现代浏览器支持自动垃圾回收部分事件监听器，但建议手动清理以确保安全。
示例：
javascript
const button = document.getElementById('button');
const handler = () => console.log('Clicked');
button.addEventListener('click', handler);
// 若移除按钮前未调用 button.removeEventListener('click', handler)，handler 及其引用的变量不会被回收

6. 弱引用场景误用强引用
原因：在需要临时缓存数据的场景中，若使用普通对象或数组存储引用，会导致对象无法被回收。应使用 WeakMap 或 WeakSet 存储临时引用，其不阻止对象被垃圾回收。
示例：
javascript
// 错误：使用 Map 导致 DOM 元素无法被回收
const map = new Map();
const element = document.getElementById('example');
map.set(element, 'some data');
// 正确：使用 WeakMap，元素可被正常回收
const weakMap = new WeakMap();
weakMap.set(element, 'some data');

7. 遗忘的 Web Worker 或异步操作
原因：创建的 Web Worker 或长时间运行的异步操作（如未终止的 Promise）若未正确清理，会持续占用内存。
示例：
javascript
const worker = new Worker('worker.js');
// 若未调用 worker.terminate()，worker 会持续运行

8. 控制台日志引用
原因：在浏览器控制台中打印对象会创建对该对象的引用，阻止其被回收。在内存分析时需注意清理控制台。
检测与避免内存泄漏的方法：
使用浏览器开发者工具：通过 Chrome DevTools 的 Memory 面板进行堆快照分析，比较不同时间点的内存使用情况，找出未释放的对象。
定期清理不再使用的引用：手动清除定时器、事件监听器，避免创建不必要的全局变量。
合理使用弱引用：在缓存场景中优先使用 WeakMap 和 WeakSet。
代码审查：检查长时间运行的组件（如单例模式）是否存在内存泄漏风险。
理解这些常见的内存泄漏场景，并在开发中注意避免，有助于提升 JavaScript 应用的性能和稳定性。`
              }
            ]
          },
          {
            question: `34.说说Javascript数字精度丢失的问题，如何解决?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，内存泄漏指的是程序中已不再使用的内存无法被垃圾回收机制释放的情况。以下是几种常见的内存泄漏场景：
1. 全局变量导致的内存泄漏
原因：在函数内部未声明直接赋值的变量，或通过 this、window 等全局对象创建的变量，会成为全局变量。全局变量不会被垃圾回收，若持续占用大量内存，可能导致泄漏。
示例：
javascript
function leak() {
  // 未声明的变量会成为全局变量
  leakedVar = 'This will leak!';
  // 等价于 window.leakedVar = 'This will leak!';
}


2. 未清理的定时器和回调函数
原因：setInterval、setTimeout 等定时器的回调函数及其中引用的变量，在定时器未被清除时不会被回收。若定时器持续运行且引用了大对象（如 DOM 元素），会导致内存泄漏。
示例：
javascript
const element = document.getElementById('leaky-element');
const interval = setInterval(() => {
  console.log(element.textContent); // 定时器持续引用 element
}, 1000);
// 若未调用 clearInterval(interval)，即使 element 被移除，内存也不会释放

3. 闭包导致的内存泄漏
原因：闭包会捕获并持有其外部函数的变量引用，若闭包长期存在（如作为事件监听器），其引用的变量无法被回收。
示例：
javascript
function createLeak() {
  const largeData = new Array(1000).fill('data');
  return function() {
    console.log(largeData.length); // 闭包引用 largeData
  };
}
const leakyFunc = createLeak(); // leakyFunc 持续引用 largeData

4. DOM 引用未释放
原因：若 JavaScript 中保留了对 DOM 元素的引用，即使该元素在 DOM 中被移除，内存也不会释放。若存在多个引用路径（如缓存、事件监听器），需确保所有引用都被清除。
示例：
javascript
const elements = [];
const element = document.getElementById('to-remove');
elements.push(element); // 数组引用了元素
document.body.removeChild(element); // DOM中移除元素，但 elements 仍保留引用

5. 未移除的事件监听器
原因：为 DOM 元素添加的事件监听器若未在元素被移除前手动清除，会导致内存泄漏。现代浏览器支持自动垃圾回收部分事件监听器，但建议手动清理以确保安全。
示例：
javascript
const button = document.getElementById('button');
const handler = () => console.log('Clicked');
button.addEventListener('click', handler);
// 若移除按钮前未调用 button.removeEventListener('click', handler)，handler 及其引用的变量不会被回收

6. 弱引用场景误用强引用
原因：在需要临时缓存数据的场景中，若使用普通对象或数组存储引用，会导致对象无法被回收。应使用 WeakMap 或 WeakSet 存储临时引用，其不阻止对象被垃圾回收。
示例：
javascript
// 错误：使用 Map 导致 DOM 元素无法被回收
const map = new Map();
const element = document.getElementById('example');
map.set(element, 'some data');
// 正确：使用 WeakMap，元素可被正常回收
const weakMap = new WeakMap();
weakMap.set(element, 'some data');

7. 遗忘的 Web Worker 或异步操作
原因：创建的 Web Worker 或长时间运行的异步操作（如未终止的 Promise）若未正确清理，会持续占用内存。
示例：
javascript
const worker = new Worker('worker.js');
// 若未调用 worker.terminate()，worker 会持续运行

8. 控制台日志引用
原因：在浏览器控制台中打印对象会创建对该对象的引用，阻止其被回收。在内存分析时需注意清理控制台。
检测与避免内存泄漏的方法：
使用浏览器开发者工具：通过 Chrome DevTools 的 Memory 面板进行堆快照分析，比较不同时间点的内存使用情况，找出未释放的对象。
定期清理不再使用的引用：手动清除定时器、事件监听器，避免创建不必要的全局变量。
合理使用弱引用：在缓存场景中优先使用 WeakMap 和 WeakSet。
代码审查：检查长时间运行的组件（如单例模式）是否存在内存泄漏风险。
理解这些常见的内存泄漏场景，并在开发中注意避免，有助于提升 JavaScript 应用的性能和稳定性。
34.说说Javascript数字精度丢失的问题，如何解决?
这是前端面试题
在 JavaScript 中，数字精度丢失是一个常见问题，主要源于其采用的 IEEE 754 双精度 64 位浮点数（Number 类型）表示法。以下是详细解释及解决方案：
一、精度丢失的原因
JavaScript 的Number类型使用 64 位双精度浮点数表示，其结构为：
符号位（1 位）：表示正负
指数位（11 位）：表示数值的指数范围
尾数位（52 位）：存储有效数字的二进制小数部分
问题核心：
二进制无法精确表示某些十进制小数：
例如，0.1的二进制表示是无限循环小数（0.0001100110011...），而 JavaScript 只能存储 52 位尾数，导致舍入误差。
大数精度丢失：
当数值超过2^53时，超出的精度位会被截断，导致整数运算也可能出现误差。
二、常见精度丢失场景
1. 小数运算误差
javascript
0.1 + 0.2 === 0.3; // false，实际结果为 0.30000000000000004
0.3 - 0.1 === 0.2; // false，实际结果为 0.19999999999999996
2. 大数精度丢失
javascript
Math.pow(2, 53) === Math.pow(2, 53) + 1; // true，超出精度范围
9007199254740992 === 9007199254740993; // true
3. 浮点数比较错误
javascript
0.1 + 0.2 > 0.3; // true，违背数学直觉
三、解决方案
1. 整数化处理（推荐）
将小数转换为整数进行运算，再转换回小数：
javascript
function add(a, b) {
  const baseNum = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b)));
  return (a * baseNum + b * baseNum) / baseNum;
}

function getDecimalLength(num) {
  const decimalPart = num.toString().split('.')[1];
  return decimalPart ? decimalPart.length : 0;
}

console.log(add(0.1, 0.2)); // 0.3
2. 使用toFixed()方法
限制结果的小数位数：
javascript
(0.1 + 0.2).toFixed(1); // "0.3"（返回字符串）
parseFloat((0.1 + 0.2).toFixed(10)); // 0.3（转为数字）
3. 第三方库（高精度计算）
Decimal.js：适用于财务计算，避免精度丢失。
javascript
const Decimal = require('decimal.js');
const a = new Decimal(0.1);
const b = new Decimal(0.2);
const sum = a.plus(b); // Decimal { s: 1, e: -1, d: [ 3 ] }
sum.toNumber(); // 0.3

Big.js：简单易用，专注于高精度计算。
javascript
const Big = require('big.js');
const sum = new Big(0.1).plus(new Big(0.2));
console.log(sum.toString()); // "0.3"

4. 大数处理（超出2^53）
使用BigInt（ES2020+）：
javascript
const a = 9007199254740992n; // 注意后缀 'n'
const b = 1n;
console.log(a + b); // 9007199254740993n

注意：BigInt不能与普通Number直接运算，需手动转换。
四、面试答题要点
解释原理：
JavaScript 使用 IEEE 754 双精度浮点数，尾数部分存储限制导致精度丢失。
十进制小数转二进制时可能出现无限循环，需舍入。
举例说明：
小数运算误差（如0.1 + 0.2 !== 0.3）。
大数精度丢失（如2^53 + 1 === 2^53）。
解决方案：
整数化运算（推荐）。
使用toFixed()控制精度。
引入第三方库（Decimal.js、Big.js）。
大数使用BigInt。
五、避免误区
不要直接比较浮点数：使用容差值（如Number.EPSILON）比较：
javascript
function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}
isEqual(0.1 + 0.2, 0.3); // true

注意业务场景：财务计算必须使用高精度库，避免使用原生浮点数。
总结
JavaScript 的数字精度丢失是底层存储机制导致的固有问题，无法完全避免，但可以通过合理的处理方式（如整数化、第三方库）来规避风险。在面试中，需清晰解释原理并给出至少两种解决方案。`
              }
            ]
          },
          {
            question: `35.举例说明你对尾递归的理解，有哪些应用场景`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `一、尾递归的概念与原理
尾递归是递归的一种特殊形式，其核心特点是：递归调用是函数的最后一个操作，且返回值直接被传递给上层函数，无需保留外层函数的上下文。这种优化使得递归调用可以重用当前栈帧，避免栈溢出，提升性能。
对比普通递归与尾递归：
javascript
// 普通递归（计算阶乘）
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // 最后一步是乘法运算，依赖外层函数的上下文
}

// 尾递归（计算阶乘）
function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTail(n - 1, n * accumulator); // 最后一步是纯递归调用，直接返回结果
}
原理差异：
普通递归：每次递归调用都需保存当前函数的局部变量和调用位置，栈深度为 O (n)，易导致栈溢出。
尾递归：编译器 / 解释器可优化为循环（称为 “尾调用优化”），栈深度始终为 O (1)，避免内存溢出。
二、应用场景
1. 数学计算（如阶乘、斐波那契数列）
阶乘：如上述示例，尾递归通过累加器（accumulator）保存中间结果，避免栈溢出。
斐波那契数列：
javascript
// 普通递归（低效，栈溢出风险）
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

// 尾递归（高效，避免栈溢出）
function fibTail(n, a = 0, b = 1) {
  if (n === 0) return a;
  return fibTail(n - 1, b, a + b);
}
2. 链表遍历与树遍历
链表反转：
javascript
function reverseList(head, prev = null) {
  if (head === null) return prev;
  const next = head.next;
  head.next = prev;
  return reverseList(next, head); // 尾递归调用
}
二叉树遍历：
javascript
function postOrderTraversal(node, result = []) {
  if (node === null) return result;
  postOrderTraversal(node.left, result);
  postOrderTraversal(node.right, result);
  result.push(node.value);
  return result; // 尾递归（最后一步返回结果）
}
3. 函数式编程中的递归操作
数组扁平化：
javascript
function flatten(arr, result = []) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      flatten(item, result); // 递归处理子数组
    } else {
      result.push(item);
    }
  }
  return result; // 尾递归返回结果
}
4. 替代循环的复杂逻辑
回溯算法（简化版）：
javascript
function backtrack(path, choices, result) {
  if (isValidSolution(path)) {
    result.push([...path]);
    return;
  }
  for (const choice of choices) {
    if (isValidChoice(choice)) {
      makeChoice(path, choice);
      backtrack(path, choices, result); // 尾递归继续探索
      undoChoice(path, choice);
    }
  }
  return result; // 尾递归返回最终结果
}
三、面试答题要点
核心概念：
尾递归是递归的优化形式，递归调用为最后一步操作。
依赖编译器的尾调用优化（TCO），避免栈溢出。
对比普通递归：
普通递归：栈深度 O (n)，易溢出；尾递归：栈深度 O (1)，性能更优。
应用场景：
数学计算（阶乘、斐波那契）。
数据结构遍历（链表、树）。
函数式编程中的复杂递归操作。
注意事项：
JavaScript 中，仅部分环境（如 Chrome V8、Node.js）支持尾调用优化。
若环境不支持，尾递归仍可能导致栈溢出，需手动转换为循环。
四、示例：手动实现尾递归优化（适用于不支持 TCO 的环境）
javascript
// 模拟尾递归优化（使用蹦床函数Trampoline）
function trampoline(fn) {
  return function(...args) {
    let result = fn(...args);
    while (typeof result === 'function') {
      result = result();
    }
    return result;
  };
}

// 使用蹦床函数优化尾递归
function sum(n, acc = 0) {
  if (n <= 0) return acc;
  return () => sum(n - 1, acc + n); // 返回一个函数而非直接递归
}

const optimizedSum = trampoline(sum);
console.log(optimizedSum(100000)); // 不会栈溢出



总结
尾递归通过优化调用栈，将递归转化为循环，提升性能并避免内存溢出。在数学计算、数据结构遍历等场景中尤为实用，但需注意环境是否支持尾调用优化。在面试中，需结合原理、示例和应用场景进行阐述。`
              }
            ]
          },
        ]
      },
      {
        tag: `数据处理`,
        data: [
          {
            question: '删除数组的第一个元素，不改变原数组，返回一个新数组',
            description: `删除数组的第一个元素，不改变原数组，返回一个新数组`,
            answer: [
              {
                text: `方法一：slice(start,end)，选取数组的一部分，返回一个新数组。`,
                daima: `
var arr = [1, 2, 3, 4, 5];
var newArr = arr.slice(1);
console.log(newArr);
`
              },
              {
                text: `方法二：filter() 过滤下标，返回满足不等于0的元素。`,
                daima: `
var arr = [1, 2, 3, 4, 5];
var newArr = arr.filter((val,index,arr) => {
    return index !== 0;
})
console.log(newArr);
`
              },
              {
                text: `方法三：push.apply() 合并数组 + shift() 方法`,
                daima: `
// push.apply: 合并数组，把后一个数组合并进前一个数组，使前一个数组发生改变。
// shift(): 方法用于把数组的第一个元素从其中删除，并返回第一个元素的值。
var arr = [1, 2, 3, 4, 5];
let newArr = [];
newArr.push.apply(newArr,arr);
newArr.shift();
console.log(newArr);
`
              }
            ]
          },
          {
            question: '数组去重',
            description: `数组去重`,
            answer: [
              {
                text: '方法一：使用 Set（ES6 特性）',
                daima: `
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]
`
              },
              {
                text: '方法二：使用 filter 和 indexOf',
                daima: `
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index, self) => {
  return self.indexOf(item) === index;
});
console.log(uniqueArr); // [1, 2, 3, 4, 5]
`
              },
              {
                text: '方法三：使用 reduce',
                daima: `
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, current) => {
  if (!acc.includes(current)) {
    acc.push(current);
  }
  return acc;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
`
              }
            ]
          },
          {
            question: '数组扁平化',
            description: `数组扁平化`,
            answer: [
              {
                text: '方法一：使用 flat() 方法（ES2019）',
                daima: `
const arr = [1, [2, [3, 4]]];
const flattened = arr.flat(Infinity);
console.log(flattened); // [1, 2, 3, 4]
`
              },
              {
                text: '方法二：使用 reduce 和 concat',
                daima: `
const arr = [1, [2, [3, 4]]];
const flattened = arr.reduce((acc, val) => 
  acc.concat(Array.isArray(val) ? flatten(val) : val), []);
console.log(flattened); // [1, 2, 3, 4]
`
              },
              {
                text: '方法三：使用扩展运算符和 some',
                daima: `
const arr = [1, [2, [3, 4]]];
let flattened = [...arr];
while (flattened.some(Array.isArray)) {
  flattened = [].concat(...flattened);
}
console.log(flattened); // [1, 2, 3, 4]
`
              }
            ]
          },
          {
            question: `1、删除数组的第一个元素，不改变原数组，返回一个新数组。`,
            description: `1、删除数组的第一个元素，不改变原数组，返回一个新数组。`,
            answer: [
              {
                text: `方法一：slice(start,end)，选取数组的一部分，返回一个新数组。`,
                daima: `var arr = [1, 2, 3, 4, 5];
var newArr = arr.slice(1);
console.log(newArr);`,
              },
              {
                text: `方法二：filter() 过滤下标，返回满足不等于0的元素。`,
                daima: `var arr = [1, 2, 3, 4, 5];
var newArr = arr.filter((val,index,arr) => {
    return index !== 0;
})
console.log(newArr);`
              },
              {
                text: `方法三：push.apply() 合并数组 + shift() 方法`,
                daima: `// push.apply: 合并数组，把后一个数组合并进前一个数组，使前一个数组发生改变。
// shift(): 方法用于把数组的第一个元素从其中删除，并返回第一个元素的值。
var arr = [1, 2, 3, 4, 5];
let newArr = [];
newArr.push.apply(newArr,arr);
newArr.shift();
console.log(newArr);
                `
              },
              {
                text: `方法四：concat() 合并数组 + shift() 方法`,
                daima: `// concat: 合并2个或多个数组，返回的是一个浅拷贝
var arr = [1, 2, 3, 4, 5];
let newArr = arr.concat();
newArr.shift();
console.log(newArr);`
              },
              {
                text: `方法五：迭代拷贝`,
                daima: `var arr = [1, 2, 3, 4, 5];
let newArr = [];
for(var i = 1; i < arr.length; i++){
   newArr.push(arr[i]);
}
console.log(newArr);`
              }
            ]
          },
          {
            question: `2、数组去重`,
            description: `2、数组去重`,
            answer: [
              {
                text: `方法一：双重for循环`,
                daima: `var arr = [1,3,2,4,5,5,6,6]; 
function noRepeat(arr){
   for (var i = 0; i < arr.length; i++) {
       for (var j = 0; j < arr.length; j++) {
           if (arr[i] == arr[j] && i != j) { 
              arr.splice(j, 1);
            }
       }
    }
    return arr;
}
console.log(noRepeat(arr));   `,
              },
              {
                text: `方法二：for 循环 + indexOf`,
                daima: `var arr = [1,3,2,4,5,5,6,6];
function noRepeat(arr) {
    let newArr = []
    for(let i = 0;i<arr.length;i++){
        newArr.indexOf(arr[i]) === -1 ? newArr.push(arr[i]) : newArr
    };
    return newArr
}
console.log(noRepeat(arr));`
              },
              {
                text: `方法三：forEach + indexOf  `,
                daima: `var arr = [1,3,2,4,5,5,6,6];	
function noRepeat(arr) {
    let newArr = [];
    arr.forEach(item=>{
        newArr.indexOf(item) === -1 ? newArr.push(item) : newArr;
    })
    return newArr;
}
console.log(noRepeat(arr));`
              },
              {
                text: `方法四：for 循环 + sort 排序 `,
                daima: `var arr = [1,3,2,4,5,5,6,6];	
function noRepeat(arr) {
    arr = arr.sort()
    let newArr = []
    for(let i = 0;i<arr.length;i++){
        arr[i] === arr[i-1] ? newArr : newArr.push(arr[i])
    };
    return newArr
}
console.log(noRepeat(arr));`
              },
              {
                text: `方法五：Set()  +  扩展运算符`,
                daima: `// ES6 提供了新的数据结构 Set,它类似于数组,成员的值都是唯一的,没有重复值。
// Set方法，返回是一个类数组，需要结合扩展运算符...转成真实数组
var arr = [1,3,2,4,5,5,6,6];	
var newArr = [...new Set(arr)];
console.log(newArr);`
              },
              {
                text: `方法六：Set()  +  Array.from()`,
                daima: `var arr = [1,3,2,4,5,5,6,6];	
var newArr = Array.from(new Set(arr));
console.log(newArr);`
              },
              {
                text: `方法七： filter + indexOf`,
                daima: `var arr = [1,3,2,4,5,5,6,6];    
let newArr = arr.filter(function(item,index,arr){ 
    return arr.indexOf(item) == index;
});
console.log(newArr);`
              }
            ]
          },
          {
            question: `3、数组冒泡排序`,
            description: `3、数组冒泡排序`,
            answer: [
              {
                text: ``,
                daima: `var arr = [12, 23, 44, 53, 67, 65, 78, 123, 2, 4, 7]
function fn(arr) {
    for (var i = 0; i < arr.length; i++) {
        for (var j = 0; j < arr.length; j++) {
            if (arr[j] < arr[j + 1]) {
                var test = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = test
            }
        }
    }
}
fn(arr)
console.log(arr);`,
              }
            ]
          },
          {
            question: ` 4、实现一个字符串转驼峰，列如border-bottom-color => borderBottomColor`,
            description: `4、实现一个字符串转驼峰，列如border-bottom-color => borderBottomColor`,
            answer: [
              {
                text: ``,
                daima: `var str = "border-bottom-color"
function toHump(str) {
    var test = str.split("-")
    for (var i = 1; i < test.length; i++) {
        test[i] = test[i][0].toUpperCase() + test[i].slice(1)
    }
    return test.join("")
}
console.log(toHump(str));`,
              }
            ]
          },
          {
            question: `5、实现一个函数计算求和，支持这两个情况：sum(2,3) 和sum(2)(3)`,
            description: `5、实现一个函数计算求和，支持这两个情况：sum(2,3) 和sum(2)(3)`,
            answer: [
              {
                text: ``,
                daima: `function sum() {
    var num = arguments[0];
    if (arguments.length == 1) {
        return function (sec) {
            console.log(num + sec);
        }
    } else {
        for (var i = 1; i < arguments.length; i++) {
            num += arguments[i]
        }
        console.log(num);
    }
}
sum(2, 3);
sum(2)(3);`,
              }
            ]
          },
          {
            question: `6、只用递归实现杨辉三角`,
            description: `6、只用递归实现杨辉三角`,
            answer: [
              {
                text: ``,
                daima: `1
1 2
1 2 3
.....
 
let num = 0;
let arr = [];
function fn(num){
    if(num<=10){
        num++;
        arr.push(num);
        console.log(...arr);
        fn(num);
	}	
}
fn(num);`,
              }
            ]
          },
          {
            question: `7、打印当前时间，时间格式为 yyyy-mm-dd`,
            description: `7、打印当前时间，时间格式为 yyyy-mm-dd`,
            answer: [
              {
                text: ``,
                daima: `let dateFormat = (dateParam, format) => {
    var day = dateParam.getDate();
    var month = dateParam.getMonth() + 1;
    var year = dateParam.getFullYear();
    format = format.replace(/yyyy/, year);
    format = format.replace(/MM/, month);
    format = format.replace(/dd/, day);
    return format;
}
// 测试代码
console.log(dateFormat(new Date('2023-10-12'), 'yyyy/MM/dd'));      // 2023/10/12
console.log(dateFormat(new Date('2023-11-12'), 'yyyy-MM-dd'));      // 2023-11-12
console.log(dateFormat(new Date('2023-12-12'), 'yyyy年MM月dd日'));  // 2023年12月12日`,
              }
            ]
          },
          {
            question: `9、实现日期格式化`,
            description: `9、实现日期格式化`,
            answer: [
              {
                text: ``,
                daima: `let dateFormat = (dateParam, format)=>{
    var day = dateParam.getDate() 
    var month = dateParam.getMonth() + 1  
    var year = dateParam.getFullYear()   
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/,month)
    format = format.replace(/dd/,day)
    return format
}
// 测试代码
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日`,
              }
            ]
          },
          {
            question: `8、判断字符串"xyzxxz"中出现次数最多的字符，并统计出现次数`,
            description: `8、判断字符串"xyzxxz"中出现次数最多的字符，并统计出现次数`,
            answer: [
              {
                text: `方法一：`,
                daima: `let str1 = "abcdefgadddaddefea"
let obj = {}
for (let i = 0; i < str1.length; i++) {
    let chars = str1.charAt(i)
    if (obj[chars]) {
        obj[chars]++;
    } else {
        obj[chars] = 1
    }
}
let max = 0;
let ch = ''
for (let key in obj) {
    if (obj[key] > max) {
        max = obj[key]
        ch = key
     }
}`,
              },
              {
                text: `方法二：`,
                daima: `var str = "zasdfghiksdsdadjadjasdksadasd";     
var arr = str.split('');  //字符串转成数组
var obj = {};
arr.forEach((item)=>{
    if(obj[item]){
        obj[item]++;
    }else{
        obj[item] = 1;
    }
})
var a = Object.values(obj); //获取对象的key的value值
var m = Math.max(...a);  //解构出最大的value值
for (key in obj){   //迭代obj对象
    if (obj[key] === m ) {
    }
}`
              }
            ]
          },
          {
            question: `9、实现数组的乱序输出`,
            description: `9、实现数组的乱序输出`,
            answer: [
              {
                text: ``,
                daima: `方法一：Fisher-Yates算法（最优解）
                function shuffle(arr) {
  const newArr = [...arr]; // 创建副本，避免修改原数组

  for (let i = newArr.length - 1; i > 0; i--) {
    // 生成 0 到 i 之间的随机索引
    const j = Math.floor(Math.random() * (i + 1));

    // 交换 i 和 j 位置的元素
    [newArr[i], newArr[j]] = [newArr[j], newArr[i]];
  }

  return newArr;
}
方法二：使用sort（非均匀随机，不推荐）
function shuffleBad(arr) {
  return [...arr].sort(() => Math.random() - 0.5);
}
方法三：递归抽取（效率比较低）
function shuffleRecursive(arr) {
  if (arr.length <= 1) return arr;

  const randomIndex = Math.floor(Math.random() * arr.length);
  const randomElement = arr[randomIndex];

  // 递归处理剩余元素
  const remaining = [...arr.slice(0, randomIndex), ...arr.slice(randomIndex + 1)];
  return [randomElement, ...shuffleRecursive(remaining)];
}`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 实现带权重的随机选择
                function weightedRandom(arr, weights) {
  const sum = weights.reduce((acc, w) => acc + w, 0);
  let random = Math.random() * sum;

  for (let i = 0; i < arr.length; i++) {
    random -= weights[i];
    if (random <= 0) return arr[i];
  }

  return arr[arr.length - 1];
}
2. 生成n个不重复的随机数
function getUniqueRandoms(min, max, n) {
  if (n > max - min + 1) throw new Error('范围不足');

  const set = new Set();
  while (set.size < n) {
    set.add(Math.floor(Math.random() * (max - min + 1)) + min);
  }

  return Array.from(set);
}`,
              }
            ]
          },
          {
            question: `10、实现数组元素求和`,
            description: `10、实现数组元素求和`,
            answer: [
              {
                text: ``,
                daima: `(1) arr=[1,2,3,4,5,6,7,8,9,10]，求和

(2) arr=[1,2,3,[[4,5],6],7,8,9]，求和

(3) arr = [{a:1, b:3}, {a:2, b:3, c:4}, {a:3}]，求和`,
              }
            ]
          },
          {
            question: `11、实现数组的扁平化`,
            description: `11、实现数组的扁平化`,
            answer: [
              {
                text: ``,
                daima: `方法一：递归+循环（基础实现）
                function flatten(arr) {
  const result = [];

  arr.forEach(item => {
    if (Array.isArray(item)) {
      // 递归展开子数组并合并到结果
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  });

  return result;
}
方法二：使用reduce（简介实现）
function flatten(arr) {
  return arr.reduce((acc, item) => {
    return acc.concat(Array.isArray(item) ? flatten(item) : item);
  }, []);
}
方法三：迭代+栈（非递归实现）
function flatten(arr) {
  const result = [];
  const stack = [...arr]; // 复制原数组到栈

  while (stack.length > 0) {
    const item = stack.pop(); // 取出栈顶元素

    if (Array.isArray(item)) {
      // 展开子数组并压入栈（注意顺序）
      stack.push(...item);
    } else {
      // 非数组元素直接添加到结果（逆序）
      result.unshift(item);
    }
  }

  return result;
}
方法四：使用flat代替
function flatten(arr) {
  return arr.flat(Infinity); // 原生方法，指定深度为无限
}`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 实现指定深度的扁平化
                function flattenDepth(arr, depth = 1) {
  if (depth === 0) return arr.slice();

  return arr.reduce((acc, item) => {
    return acc.concat(
      Array.isArray(item)
        ? flattenDepth(item, depth - 1)
        : item
    );
  }, []);
}
2. 扁平化对象的数组属性
function flattenObjectArrays(obj) {
  const result = {};

  for (const [key, value] of Object.entries(obj)) {
    if (Array.isArray(value)) {
      result[key] = flatten(value);
    } else if (typeof value === 'object' && value !== null) {
      result[key] = flattenObjectArrays(value);
    } else {
      result[key] = value;
    }
  }

  return result;
}`,
              }
            ]
          },
          {
            question: `12、实现对象扁平化`,
            description: `12、实现对象扁平化`,
            answer: [
              {
                text: ``,
                daima: `方法一：递归实现（基础版）
                function flattenObject(obj, parentKey = '', result = {}) {
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const newKey = parentKey ? '￥{parentKey}.￥{key}'' : key;
      const value = obj[key];
      
      if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
        // 递归处理嵌套对象
        flattenObject(value, newKey, result);
      } else {
        // 基本类型或数组直接赋值
        result[newKey] = value;
      }
    }
  }
  
  return result;
}
方法二：迭代实现（使用栈）
function flattenObject(obj) {
  const result = {};
  const stack = [{ current: obj, parentKey: '' }];
  
  while (stack.length > 0) {
    const { current, parentKey } = stack.pop();
    
    for (const key in current) {
      if (current.hasOwnProperty(key)) {
        const newKey = parentKey ? '￥{parentKey}.￥{key}' : key;
        const value = current[key];
        
        if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
          // 入栈待处理
          stack.push({ current: value, parentKey: newKey });
        } else {
          result[newKey] = value;
        }
      }
    }
  }
  
  return result;
}
方法三：处理数组和特殊情况
function flattenObject(obj, options = {}) {
  const { maxDepth = Infinity, preserveArrays = false } = options;
  const result = {};
  
  function flatten(current, path, depth) {
    for (const key in current) {
      if (current.hasOwnProperty(key)) {
        const newPath = path ? '￥{path}.￥{key}' : key;
        const value = current[key];
        const isObject = typeof value === 'object' && value !== null;
        const isArray = Array.isArray(value);
        
        if (
          isObject && 
          (!isArray || !preserveArrays) && 
          depth < maxDepth
        ) {
          flatten(value, newPath, depth + 1);
        } else {
          result[newPath] = value;
        }
      }
    }
  }
  
  flatten(obj, '', 0);
  return result;
}`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 反向操作：对象反扁平化
                function unflattenObject(obj) {
  const result = {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const parts = key.split('.');
      let current = result;

      for (let i = 0; i < parts.length - 1; i++) {
        const part = parts[i];
        if (!current[part]) {
          current[part] = {};
        }
        current = current[part];
      }

      current[parts[parts.length - 1]] = obj[key];
    }
  }

  return result;
}
2. 处理循环引用
function flattenObjectWithCircular(obj) {
  const result = {};
  const seen = new WeakSet();
  
  function flatten(current, path) {
    if (seen.has(current)) {
      result[path] = '[Circular]';
      return;
    }
    
    seen.add(current);
    
    for (const key in current) {
      if (current.hasOwnProperty(key)) {
        const newPath = path ? '￥{path}.￥{key}' : key;
        const value = current[key];
        
        if (typeof value === 'object' && value !== null) {
          flatten(value, newPath);
        } else {
          result[newPath] = value;
        }
      }
    }
  }
  
  flatten(obj, '');
  return result;
}
                `,
              }
            ]
          },
          {
            question: `13、实现数组的 flat 方法`,
            description: `13、实现数组的 flat 方法`,
            answer: [
              {
                text: ``,
                daima: `方法一：递归+循环，最基础的实现
                Array.prototype.myFlat = function(depth = 1) {
  const result = [];

  this.forEach(item => {
    if (Array.isArray(item) && depth > 0) {
      // 递归展开子数组，深度减1
      result.push(...item.myFlat(depth - 1));
    } else {
      // 非数组或深度为0时直接添加
      result.push(item);
    }
  });

  return result;
};
方法二：使用栈（迭代实现）
Array.prototype.myFlat = function(depth = 1) {
  const result = [];
  const stack = [...this].map(item => ({ value: item, depth: 0 }));

  while (stack.length > 0) {
    const { value, depth: currentDepth } = stack.pop();

    if (Array.isArray(value) && currentDepth < depth) {
      // 展开子数组，保持原顺序
      value.forEach(item => {
        stack.push({ value: item, depth: currentDepth + 1 });
      });
    } else {
      // 非数组或深度达到上限时添加到结果
      result.unshift(value);
    }
  }

  return result;
};
方法三：使用reduce和concat简介实现
Array.prototype.myFlat = function(depth = 1) {
  return depth > 0
    ? this.reduce((acc, val) =>
        acc.concat(Array.isArray(val) ? val.myFlat(depth - 1) : val),
        []
      )
    : this.slice(); // 深度为0时直接返回原数组副本
};`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 实现flatmap
                Array.prototype.myFlatMap = function(callback) {
  return this.map(callback).myFlat();
};
扁平化对象（非数组）
function flattenObject(obj) {
  const result = {};
  
  function recurse(current, path) {
    for (const key in current) {
      const newPath = path ? '￥{path}.￥{key}' : key;
      const value = current[key];
      
      if (typeof value === 'object' && value !== null) {
        recurse(value, newPath);
      } else {
        result[newPath] = value;
      }
    }
  }
  
  recurse(obj, '');
  return result;
}
                `
              }
            ]
          },
          {
            question: `14、实现数组的 push 方法`,
            description: `14、实现数组的 push 方法`,
            answer: [
              {
                text: `方法实现`,
                daima: `Array.prototype.myPush = function(...items) {
  // 1. 获取原数组的长度
  const len = this.length >>> 0; // 确保 len 为正整数或 0
  
  // 2. 处理要添加的元素
  const argCount = items.length;
  
  // 3. 检查是否需要扩容（JavaScript 数组会自动扩容）
  
  // 4. 将元素添加到数组末尾
  for (let i = 0; i < argCount; i++) {
    this[len + i] = items[i];
  }
  
  // 5. 返回新的长度
  return len + argCount;
};`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 模拟push但返回数组本身
                Array.prototype.myPushAndReturn = function(...items) {
  const len = this.length;
  for (let i = 0; i < items.length; i++) {
    this[len + i] = items[i];
  }
  return this; // 返回数组本身而非长度
};
2. 实现push的类型安全版本
Array.prototype.typeSafePush = function(...items) {
  if (items.some(item => typeof item !== typeof this[0])) {
    throw new TypeError('All elements must be of the same type');
  }
  return this.myPush(...items);
};`,
              }
            ]
          },
          {
            question: `15、实现数组的 filter 方法`,
            description: `15、实现数组的 filter 方法`,
            answer: [
              {
                text: `方法实现`,
                daima: `Array.prototype.myFilter = function(callback, thisArg) {
  // 1. 检查 this 是否为 null 或 undefined
  if (this == null) {
    throw new TypeError('Array cannot be null or undefined');
  }
  
  // 2. 将 this 转换为对象（处理原始值调用）
  const arr = Object(this);
  
  // 3. 获取数组长度（处理稀疏数组）
  const len = arr.length >>> 0; // 确保 len 为正整数或 0
  
  // 4. 检查 callback 是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  // 5. 创建新数组存储结果
  const result = [];
  
  // 6. 遍历数组，跳过不存在的索引（稀疏数组）
  for (let i = 0; i < len; i++) {
    if (i in arr) { // 仅处理已定义的索引
      // 执行回调函数并判断是否保留元素
      if (callback.call(thisArg, arr[i], i, arr)) {
        result.push(arr[i]);
      }
    }
  }
  
  return result;
};`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 链式调用
                Array.prototype.myFilter = function(callback) {
  // ...（实现同上）
  return result;
};

// 测试链式调用
[1, 2, 3, 4].myFilter(x => x > 2).myFilter(x => x % 2 === 0); // [4]
2. 模拟filter但返回对象
Array.prototype.filterToObject = function(callback) {
  const obj = {};
  this.forEach((item, index) => {
    if (callback(item, index, this)) {
      obj[index] = item;
    }
  });
  return obj;
};

// 示例
[1, 2, 3].filterToObject(x => x > 1); // { 1: 2, 2: 3 }`,
              }
            ]
          },
          {
            question: `16、实现数组的 map 方法`,
            description: `16、实现数组的 map 方法`,
            answer: [
              {
                text: `方法实现`,
                daima: `Array.prototype.myMap = function(callback, thisArg) {
  // 1. 检查 this 是否为 null 或 undefined
  if (this == null) {
    throw new TypeError('Array cannot be null or undefined');
  }
  
  // 2. 将 this 转换为对象（处理原始值调用）
  const arr = Object(this);
  
  // 3. 获取数组长度（处理稀疏数组）
  const len = arr.length >>> 0; // 确保 len 为正整数或 0
  
  // 4. 检查 callback 是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  // 5. 创建新数组存储结果
  const result = new Array(len);
  
  // 6. 遍历数组，跳过不存在的索引（稀疏数组）
  for (let i = 0; i < len; i++) {
    if (i in arr) { // 仅处理已定义的索引
      // 执行回调函数并存储结果
      result[i] = callback.call(thisArg, arr[i], i, arr);
    }
  }
  
  return result;
};`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 链式调用
                Array.prototype.myMap = function(callback) {
  // ...（实现同上）
  return result;
};

// 测试链式调用
[1, 2, 3].myMap(x => x + 1).myMap(x => x * 2); // [4, 6, 8]
2. 模拟map但返回对象
Array.prototype.toObject = function(callback) {
  const obj = {};
  this.forEach((item, index) => {
    const [key, value] = callback(item, index, this);
    obj[key] = value;
  });
  return obj;
};

// 示例
['a', 'b'].toObject((item, index) => [item, index]); // { a: 0, b: 1 }`,
              }
            ]
          },
          {
            question: `17、实现字符串的 repeat 方法`,
            description: `17、实现字符串的 repeat 方法`,
            answer: [
              {
                text: ``,
                daima: `方法一：循环拼接，最基础的实现
                function repeat(str, count) {
  if (count < 0 || !Number.isInteger(count)) {
    throw new RangeError('Count must be a non-negative integer');
  }

  let result = '';
  for (let i = 0; i < count; i++) {
    result += str;
  }
  return result;
}
方法二：递归拼接，减少循环次数
function repeat(str, count) {
  if (count < 0 || !Number.isInteger(count)) {
    throw new RangeError('Count must be a non-negative integer');
  }

  if (count === 0) return '';
  if (count === 1) return str;

  const half = repeat(str, Math.floor(count / 2));
  return count % 2 === 0 ? half + half : half + half + str;
}
方法三：位运算优化：
function repeat(str, count) {
  if (count < 0 || !Number.isInteger(count)) {
    throw new RangeError('Count must be a non-negative integer');
  }

  let result = '';
  let multiplier = count;

  while (multiplier > 0) {
    if (multiplier % 2 === 1) {
      result += str;
    }
    if (multiplier > 1) {
      str += str;
    }
    multiplier = Math.floor(multiplier / 2);
  }

  return result;
}`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 实现repeat但是限制最大长度
                function repeatLimited(str, count, maxLength) {
  let result = '';
  for (let i = 0; i < count; i++) {
    if (result.length + str.length > maxLength) {
      return result + str.slice(0, maxLength - result.length);
    }
    result += str;
  }
  return result;
}
2. 使用位运算实现repeat
function repeat(str, count) {
  let result = '';
  while (count > 0) {
    if (count & 1) result += str; // 奇数时拼接
    str += str; // 每次翻倍
    count >>>= 1; // 右移一位（相当于除以2取整）
  }
  return result;
}`,
              }
            ]
          },
          {
            question: `18、实现类数组转化为数组`,
            description: `18、实现类数组转化为数组`,
            answer: [
              {
                text: ``,
                daima: `方法一：使用Array.form()差不多是最简单的方法了
                function convertToArray(arrayLike) {
  return Array.from(arrayLike);
}

// 示例
function test() {
  return convertToArray(arguments);
}

const arr = test(1, 2, 3);
console.log(arr); // [1, 2, 3]
console.log(arr instanceof Array); // true
方法二：使用扩展运算符的方式
function convertToArray(arrayLike) {
  return [...arrayLike];
}

// 示例
const nodeList = document.querySelectorAll('div');
const arr = convertToArray(nodeList);
console.log(arr.map(div => div.tagName)); // ['DIV', 'DIV', ...]
方法三：使用Array.protorype.slice.call()
function convertToArray(arrayLike) {
  return Array.prototype.slice.call(arrayLike);
}

// 或简写为
const convertToArray = Function.call.bind(Array.prototype.slice);
方法四：手动遍历兼容性最佳，但是不太好写
function convertToArray(arrayLike) {
  const result = [];
  for (let i = 0; i < arrayLike.length; i++) {
    result.push(arrayLike[i]);
  }
  return result;
}
需要注意的地方：
对于系数数组，稀疏数组处理：
Array.from() 和手动遍历会保留稀疏数组的空缺位。
slice 和扩展运算符会将空缺位填充为 undefined。
const arrayLike = { 0: 'a', 2: 'c', length: 3 };

Array.from(arrayLike); // ['a', undefined, 'c']
[...arrayLike]; // ['a', undefined, 'c']
Array.prototype.slice.call(arrayLike); // ['a', undefined, 'c']

// 手动遍历
const result = [];
for (let i = 0; i < arrayLike.length; i++) {
  result.push(arrayLike[i]);
}
console.log(result); // ['a', undefined, 'c']
`,
              },
              {
                text: `其他的相关的面试题`,
                daima: `1. 反向操作：数组转类数组
                function arrayToArrayLike(arr) {
  return Object.assign({ length: arr.length }, arr);
}

const arr = [1, 2, 3];
const arrayLike = arrayToArrayLike(arr);
console.log(arrayLike); // { '0': 1, '1': 2, '2': 3, length: 3 }
2. 自定义数组转数组
const myArrayLike = {
  0: 'hello',
  1: 'world',
  length: 2,
  sayHi: function() { console.log('Hi'); }
};

const arr = Array.from(myArrayLike);
console.log(arr); // ['hello', 'world']`,
              }
            ]
          },
          {
            question: `19、 将js对象转化为树形结构`,
            description: `19、 将js对象转化为树形结构`,
            answer: [
              {
                text: ``,
                daima: `方法一：递归查找（适合小规模数据）
                function arrayToTree(items, id = null, link = 'parentId') {
  return items
    .filter(item => item[link] === id)
    .map(item => ({
      ...item,
      children: arrayToTree(items, item.id, link)
    }));
}

// 示例数据
const data = [
  { id: 1, name: '部门A', parentId: null },
  { id: 2, name: '部门B', parentId: 1 },
  { id: 3, name: '部门C', parentId: 1 },
  { id: 4, name: '部门D', parentId: 2 },
  { id: 5, name: '部门E', parentId: 2 }
];

console.log(arrayToTree(data));
/* 输出:
[
  {
    id: 1,
    name: '部门A',
    parentId: null,
    children: [
      {
        id: 2,
        name: '部门B',
        parentId: 1,
        children: [
          { id: 4, name: '部门D', parentId: 2, children: [] },
          { id: 5, name: '部门E', parentId: 2, children: [] }
        ]
      },
      { id: 3, name: '部门C', parentId: 1, children: [] }
    ]
  }
]
*/
                `,
              },
              {
                text: `方法二：Map优化，适合大规模数据`,
                daima: `function arrayToTree(items) {
  const map = new Map();
  const result = [];

  // 第一次遍历：将每个节点存入 Map
  items.forEach(item => {
    map.set(item.id, { ...item, children: [] });
  });

  // 第二次遍历：构建树结构
  items.forEach(item => {
    const node = map.get(item.id);
    if (item.parentId === null) {
      // 根节点
      result.push(node);
    } else {
      // 子节点：将当前节点添加到父节点的 children 中
      const parent = map.get(item.parentId);
      if (parent) {
        parent.children.push(node);
      }
    }
  });

  return result;
}`,
              },
              {
                text: `方法三：对象引用适合复杂场景`,
                daima: `function arrayToTree(items) {
  const idMap = {};
  const tree = [];

  // 第一次遍历：创建节点引用并建立 id 到节点的映射
  items.forEach(item => {
    idMap[item.id] = { ...item, children: [] };
  });

  // 第二次遍历：连接父子节点
  items.forEach(item => {
    const node = idMap[item.id];
    if (item.parentId === null) {
      tree.push(node);
    } else {
      const parent = idMap[item.parentId];
      if (parent) {
        parent.children.push(node);
      } else {
        // 处理父节点不存在的情况（可根据需求调整）
        tree.push(node);
      }
    }
  });

  return tree;
}`,
              },
              {
                text: `面试的变种题目`,
                daima: `1. 属性结构转扁平数组
                function treeToArray(tree) {
  return tree.reduce((acc, node) => {
    const { children, ...rest } = node;
    return [...acc, rest, ...treeToArray(children)];
  }, []);
}
2. 根据ID查找节点
function findNode(tree, id) {
  for (const node of tree) {
    if (node.id === id) return node;
    const found = findNode(node.children, id);
    if (found) return found;
  }
  return null;
}
3. 计算节点深度
function calculateDepth(tree, depth = 0) {
  return tree.reduce((max, node) => {
    const childDepth = calculateDepth(node.children, depth + 1);
    return Math.max(max, childDepth);
  }, depth);
}`,
              }
            ]
          },
          {
            question: `20、 解析 URL Params 为对象`,
            description: `20、 解析 URL Params 为对象`,
            answer: [
              {
                text: `使用URLSearchParams方法`,
                daima: `function parseURLParams(url) {
  const params = new URLSearchParams(url.split('?')[1]);
  const result = {};
  
  params.forEach((value, key) => {
    // 处理重复参数（如 ?key=val1&key=val2）
    if (result[key]) {
      if (!Array.isArray(result[key])) {
        result[key] = [result[key]];
      }
      result[key].push(value);
    } else {
      result[key] = value;
    }
  });
  
  return result;
}

// 示例
const url = 'https://example.com?name=John&age=30&hobbies=reading&hobbies=coding';
console.log(parseURLParams(url)); 
// 输出: { name: 'John', age: '30', hobbies: ['reading', 'coding'] }`,
              },
              {
                text: `手动解析，兼容性比较好`,
                daima: `function parseURLParams(url) {
  const params = {};
  const queryString = url.split('?')[1];
  
  if (!queryString) return params;
  
  const keyValuePairs = queryString.split('&');
  
  for (const pair of keyValuePairs) {
    const [key, value] = pair.split('=').map(decodeURIComponent);
    
    if (key === undefined || value === undefined) continue;
    
    // 处理重复参数
    if (params[key] !== undefined) {
      if (!Array.isArray(params[key])) {
        params[key] = [params[key]];
      }
      params[key].push(value);
    } else {
      params[key] = value;
    }
  }
  
  return params;
}

// 示例
const url = 'https://example.com?name=John%20Doe&age=30&hobbies=reading&hobbies=coding';
console.log(parseURLParams(url)); 
// 输出: { name: 'John Doe', age: '30', hobbies: ['reading', 'coding'] }`,
              },
              {
                text: `面试的变种题`,
                daima: `1. 反向操作，将对象转换为URL参数
                function objectToQueryString(obj) {
  return Object.entries(obj)
    .map(([key, value]) => {
      if (Array.isArray(value)) {
        return value.map(v => '￥{encodeURIComponent(key)} = ￥{encodeURIComponent(v)}').join('&');
      }
      return '￥{encodeURIComponent(key)} = ￥{encodeURIComponent(value)}';
    })
    .join('&');
}
2. 处理哈希的情况
function parseHashParams(url) {
  const hash = url.split('#')[1];
  return hash ? parseURLParams('? ￥{hash}'') : {};
}`,
              }
            ]
          },
        ]
      },
      {
        tag: `场景题`,
        data: [
          {
            question: `1、循环打印红黄绿`,
            description: `红灯 3s 亮一次，绿灯 1s 亮一次，黄灯 2s 亮一次`,
            answer: [
              {
                text: `(1) 用 promise 实现`,
                daima: `
function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

const task = (timer, light) =>
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })

const step = () => {
    task(3000, 'red')
        .then(() => task(1000, 'green'))
        .then(() => task(2000, 'yellow'))
        .then(step)
}
step()
`
              },
              {
                text: `(2) 用 async/await 实现`,
                daima: `
function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

const task = (timer, light) =>
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })

const step = async () => {
    await task(3000, 'red')
    await task(1000, 'green')
    await task(2000, 'yellow')
    step()
}
step()
`
              }
            ]
          },
          {
            question: '实现一个简单的 Promise',
            description: `实现一个简单的 Promise`,
            answer: [
              {
                text: 'Promise 有三种状态：pending、fulfilled、rejected，状态一旦改变就不能再变。',
                daima: `
class MyPromise {
  constructor(executor) {
    // 初始状态为 pending
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    
    // 存储成功和失败的回调
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    
    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 执行所有成功回调
        this.onFulfilledCallbacks.forEach(callback => callback());
      }
    };
    
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 执行所有失败回调
        this.onRejectedCallbacks.forEach(callback => callback());
      }
    };
    
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  
  then(onFulfilled, onRejected) {
    // 处理默认回调
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error; };
    
    // 创建新的 Promise 实现链式调用
    const newPromise = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      
      const handleRejected = () => {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      
      if (this.status === 'fulfilled') {
        setTimeout(handleFulfilled, 0);
      } else if (this.status === 'rejected') {
        setTimeout(handleRejected, 0);
      } else {
        // 状态还是 pending，存储回调
        this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
        this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
      }
    });
    
    return newPromise;
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }
  
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}
`
              }
            ]
          },
          {
            question: '实现一个简单的 Event Emitter',
            description: `实现一个简单的 Event Emitter`,
            answer: [
              {
                text: 'Event Emitter 是 Node.js 中的核心模块，用于实现事件驱动编程。它基于发布-订阅模式。',
                daima: `
class EventEmitter {
  constructor() {
    // 存储事件和对应的回调函数
    this.events = {};
  }
  
  // 注册事件监听器
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
    return this;
  }
  
  // 触发事件
  emit(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => {
        callback.apply(this, args);
      });
    }
    return this;
  }
  
  // 移除事件监听器
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    }
    return this;
  }
  
  // 只执行一次的事件监听器
  once(eventName, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
    return this;
  }
}

// 使用示例
const emitter = new EventEmitter();

// 注册事件监听器
emitter.on('message', (data) => {
  console.log('接收到消息:', data);
});

// 触发事件
emitter.emit('message', 'Hello, world!'); // 输出: 接收到消息: Hello, world!
`
              }
            ]
          },
          {
            question: `2、实现每隔一秒打印 1、2、3、4`,
            description: `2、实现每隔一秒打印 1、2、3、4`,
            answer: [
              {
                text: `(1) 使用闭包实现`,
                daima: `for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}`,
              },
              {
                text: `(2) 使用 let 块级作用域实现`,
                daima: `for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}`
              }
            ]
          },
          {
            question: ` 3、实现斐波那契数列`,
            description: `3、实现斐波那契数列`,
            answer: [
              {
                text: ``,
                daima: `代码自己去找`,
              }
            ]
          },
          {
            question: `4、使用 setTimeout 实现 setInterval`,
            description: `4、使用 setTimeout 实现 setInterval`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `5、实现简单路由`,
            description: `5、实现简单路由`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `6、实现双向数据绑定`,
            description: `6、实现双向数据绑定`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `7、实现 prototype 继承`,
            description: `7、实现 prototype 继承`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `8、实现发布-订阅模式`,
            description: `8、实现发布-订阅模式`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `9、用 Promise 实现图片的异步加载`,
            description: `9、用 Promise 实现图片的异步加载`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `10、手写一个 jsonp`,
            description: `10、手写一个 jsonp`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `11、实现事件侦听器函数`,
            description: `11、实现事件侦听器函数`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `12、实现一个事件委托 `,
            description: `12、实现一个事件委托`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `13、实现一个异步调度器`,
            description: `13、实现一个异步调度器`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `14、实现一个可以拖拽的 div`,
            description: `14、实现一个可以拖拽的 div`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `15、实现一个 sleep 函数`,
            description: `15、实现一个 sleep 函数`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `16、实现寄生组合继承`,
            description: `16、实现寄生组合继承`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `17、冒泡排序`,
            description: `17、冒泡排序`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `18、插入排序`,
            description: `18、插入排序`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `19、选择排序`,
            description: `19、选择排序`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `20、快速排序`,
            description: `20、快速排序`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `21、归并排序`,
            description: `21、归并排序`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `22、二分法查`,
            description: `22、二分法查`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
          {
            question: `23、LRU 缓存淘汰算法`,
            description: `23、LRU 缓存淘汰算法`,
            answer: [
              {
                text: `代码自己去找`,
                daima: ``,
              }
            ]
          },
        ]
      },
      {
        tag: `150面试题目`,
        data: [
          {
            question: `6Z形变换`,
            description: `将一个给定字符串 s 根据给定的行数 numRows ，以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时，排列如下：

P   A   H   N
A P L S I I G
Y   I   R
之后，你的输出需要从左往右逐行读取，产生出一个新的字符串，比如："PAHNAPLSIIGYIR"。

请你实现这个将字符串进行指定行数变换的函数：

string convert(string s, int numRows);`,
            answer: [
              {
                text: ``,
                daima: `var convert = function (s, numRows) {
  // 处理特殊情况：当行数为 1 时，直接返回原字符串
  if (numRows === 1) return s

  // 确定实际需要的行数，取字符串长度和指定行数中的较小值
  let len = Math.min(s.length, numRows)
  // 创建一个数组，每个元素代表一行，初始化为空字符串
  let num = new Array(numRows).fill('')

  // 当前处理的行索引
  let index = 0
  // 方向标志：true 表示向下移动，false 表示向上移动
  let down = false

  // 遍历字符串中的每个字符
  for (let c of s) {
    // 将当前字符添加到对应行的字符串中
    num[index] += c

    // 当到达第一行或最后一行时，改变移动方向
    if (index === 0 || index === numRows - 1) {
      down = !down
    }

    // 根据方向更新行索引：向下移动则加 1，向上移动则减 1
    index += down ? 1 : -1
  }

  // 将所有行的字符串连接起来，得到最终结果
  return num.join('')
}`
              }
            ]
          },
          {
            question: `88. 合并两个有序数组`,
            description: `给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2，另有两个整数 m 和 n ，分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中，使合并后的数组同样按 非递减顺序 排列。

注意：最终，合并后数组不应由函数返回，而是存储在数组 nums1 中。为了应对这种情况，nums1 的初始长度为 m + n，其中前 m 个元素表示应合并的元素，后 n 个元素为 0 ，应忽略。nums2 的长度为 n 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function (nums1, m, nums2, n) {
  nums1.splice(m, n, ...nums2);
  nums1.sort((a, b) => a - b);
};`
              }
            ]
          },
          {
            question: `27. 移除元素`,
            description: `给你一个数组 nums 和一个值 val，你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k，要通过此题，您需要执行以下操作：

更改 nums 数组，使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
返回 k。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function (nums, val) {
  let slow = 0;
  let len = nums.length;
  for (let fast = 0; fast < len; fast++) {
    if (nums[fast] !== val) {
      nums[slow] = nums[fast];
      slow++;
    }
  }
  return slow;
};`
              }
            ]
          },
          {
            question: `26. 删除有序数组中的重复项`,
            description: `给你一个 非严格递增排列 的数组 nums ，请你 原地 删除重复出现的元素，使每个元素 只出现一次 ，返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ，你需要做以下事情确保你的题解可以被通过：

更改数组 nums ，使 nums 的前 k 个元素包含唯一元素，并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function (nums) {
  let slow = 0;
  let len = nums.length;
  if (len === 1) return 1;
  for (let fast = 1; fast < len; fast++) {
    if (nums[fast - 1] !== nums[fast]) {
      slow++;
      nums[slow] = nums[fast]
    }
  }
  return slow+1;
};`
              }
            ]
          },
          {
            question: `169. 多数元素`,
            description: `给定一个大小为 n 的数组 nums ，返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的，并且给定的数组总是存在多数元素。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function (nums) {
  let map = {};
  let len = nums.length;
  if(len===1)return nums[0]
  for (let i = 0; i < len; i++) {
    if (map[nums[i]]) {
      map[nums[i]]++;
      if(map[nums[i]]>len/2)return nums[i]
    } else {
      map[nums[i]] = 1;
    }
  }
};`
              }
            ]
          },
          {
            question: `189. 轮转数组`,
            description: `给定一个整数数组 nums，将数组中的元素向右轮转 k 个位置，其中 k 是非负数。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @param {number} k
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var rotate = function (nums, k) {
  let temp = [...nums];
  let len=nums.length;
  for(let i=0;i<len;i++){
    nums[(k+i)%len]=temp[i]
  }
};`
              }
            ]
          },
          {
            question: `121. 买卖股票的最佳时机`,
            description: `给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let min = Infinity;
  let max=0;
  let len=prices.length;
  for(let i=0;i<len;i++){
    if(min>=prices[i]){
      min=prices[i];
    }
    if(max<=prices[i]-min){
      max=Math.max(max,prices[i]-min);
    }
  }
  return max;
};`
              }
            ]
          },
          {
            question: `122. 买卖股票的最佳时机 II`,
            description: `给你一个整数数组 prices ，其中 prices[i] 表示某支股票第 i 天的价格。

在每一天，你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买，然后在 同一天 出售。

返回 你能获得的 最大 利润 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let maxprice = 0;
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) {
      maxprice += prices[i] - prices[i - 1]
    }
  }
  return maxprice;
};`
              }
            ]
          },
          {
            question: `55. 跳跃游戏`,
            description: `给你一个非负整数数组 nums ，你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标，如果可以，返回 true ；否则，返回 false 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function (nums) {
  let max = 0;
  for (let i = 0; i < nums.length; i++) {
    if (max >= i) {
      max = Math.max(max, i + nums[i]);
      if (max >= nums.length-1) {
        return true;
      }
    }
  }
  return false;
};`
              }
            ]
          },
          {
            question: `45. 跳跃游戏 II`,
            description: `给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说，如果你在 nums[i] 处，你可以跳转到任意 nums[i + j] 处:

0 <= j <= nums[i] 
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number}
 */
var jump = function (nums) {
  let len = nums.length;
  let maxPosition = 0;
  let end = 0;
  let steps = 0;
  for (let i = 0; i < len - 1; i++) {
    maxPosition = Math.max(maxPosition, i + nums[i])
    if (i === end) {
      end = maxPosition;
      steps++;
    }
  }
  return steps;
};`
              }
            ]
          },
          {
            question: `274. H 指数`,
            description: `给你一个整数数组 citations ，其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。

根据维基百科上 h 指数的定义：h 代表“高引用次数” ，一名科研人员的 h 指数 是指他（她）至少发表了 h 篇论文，并且 至少 有 h 篇论文被引用次数大于等于 h 。如果 h 有多种可能的值，h 指数 是其中最大的那个。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} citations
 * @return {number}
 */
var hIndex = function (citations) {
  citations.sort((a, b) => a - b);
  let i = citations.length - 1;
  let h = 0;
  while (i >= 0 && citations[i] > h) {
    h++;
    i--;
  }
  return h;
};`
              }
            ]
          },
          {
            question: `380. O(1) 时间插入、删除和获取随机元素`,
            description: `实现RandomizedSet 类：

RandomizedSet() 初始化 RandomizedSet 对象
bool insert(int val) 当元素 val 不存在时，向集合中插入该项，并返回 true ；否则，返回 false 。
bool remove(int val) 当元素 val 存在时，从集合中移除该项，并返回 true ；否则，返回 false 。
int getRandom() 随机返回现有集合中的一项（测试用例保证调用此方法时集合中至少存在一个元素）。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数，并满足每个函数的 平均 时间复杂度为 O(1) 。`,
            answer: [
              {
                text: ``,
                daima: `
var RandomizedSet = function () {
  this.arr = [];
  return null;
};

/** 
 * @param {number} val
 * @return {boolean}
 */
RandomizedSet.prototype.insert = function (val) {
  if (!this.arr.includes(val)) {
    this.arr.push(val);
    return true;
  }
  return false;
};

/** 
 * @param {number} val
 * @return {boolean}
 */
RandomizedSet.prototype.remove = function (val) {
  if (this.arr.includes(val)) {
    this.arr.splice(this.arr.indexOf(val), 1);
    return true;
  }
  return false;
};

/**
 * @return {number}
 */
RandomizedSet.prototype.getRandom = function () {
  let len = this.arr.length;
  let random = Math.floor(Math.random() * len);
  return this.arr[random]
};

/** 
 * Your RandomizedSet object will be instantiated and called as such:
 * var obj = new RandomizedSet()
 * var param_1 = obj.insert(val)
 * var param_2 = obj.remove(val)
 * var param_3 = obj.getRandom()
 */`
              }
            ]
          },
          {
            question: `238. 除自身以外数组的乘积`,
            description: `给你一个整数数组 nums，返回 数组 answer ，其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请 不要使用除法，且在 O(n) 时间复杂度内完成此题。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number[]}
 */
var productExceptSelf = function (nums) {
  let len = nums.length;
  let leftarr = [];
  let rightarr = [];

  leftarr[0] = 1;
  for (let i = 1; i < len; i++) {
    leftarr[i] = leftarr[i - 1] * nums[i - 1];
  }
  rightarr[len - 1] = 1;
  for (let i = len - 2; i >= 0; i--) {
    rightarr[i] = rightarr[i + 1] * nums[i + 1];
  }
  let res = [];
  for (let i = 0; i < len; i++) {
    res[i] = leftarr[i] * rightarr[i];
  }
  return res;
};`
              }
            ]
          },
          {
            question: `134. 加油站`,
            description: `在一条环路上有 n 个加油站，其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车，从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发，开始时油箱为空。

给定两个整数数组 gas 和 cost ，如果你可以按顺序绕环路行驶一周，则返回出发时加油站的编号，否则返回 -1 。如果存在解，则 保证 它是 唯一 的。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} gas
 * @param {number[]} cost
 * @return {number}
 */
var canCompleteCircuit = function (gas, cost) {
  let totalTank = 0;
  let currentTank = 0;
  let startingStation = 0;
  for (let i = 0; i < gas.length; i++) {
    let temp = gas[i] - cost[i];
    currentTank += temp;
    totalTank += temp;

    if (currentTank < 0) {
      startingStation = i + 1;
      currentTank = 0;
    }
  }
  return totalTank >= 0 ? startingStation : -1;
};`
              }
            ]
          },
          {
            question: `13. 罗马数字转整数`,
            description: `罗马数字包含以下七种字符: I， V， X， L，C，D 和 M。

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000
例如， 罗马数字 2 写做 II ，即为两个并列的 1 。12 写做 XII ，即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下，罗马数字中小的数字在大的数字的右边。但也存在特例，例如 4 不写做 IIII，而是 IV。数字 1 在数字 5 的左边，所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地，数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况：

I 可以放在 V (5) 和 X (10) 的左边，来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边，来表示 40 和 90。 
C 可以放在 D (500) 和 M (1000) 的左边，来表示 400 和 900。
给定一个罗马数字，将其转换成整数。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function (s) {
  let mymap = new Map();
  mymap.set('I', 1);
  mymap.set('V', 5);
  mymap.set('X', 10);
  mymap.set('L', 50);
  mymap.set('C', 100);
  mymap.set('D', 500);
  mymap.set('M', 1000);

  let sum = 0;
  let len = s.length;
  for (let i = 0; i < len; i++) {
    let value = mymap.get(s[i]);
    if (i >= 0 && value < mymap.get(s[i + 1])) {
      sum -= value;
    } else {
      sum += value;
    }
  }
  return sum;
};`
              }
            ]
          },
          {
            question: `12. 整数转罗马数字`,
            description: `七个不同的符号代表罗马数字，其值如下：

符号	值
I	1
V	5
X	10
L	50
C	100
D	500
M	1000
罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规则：

如果该值不是以 4 或 9 开头，请选择可以从输入中减去的最大值的符号，将该符号附加到结果，减去其值，然后将其余部分转换为罗马数字。
如果该值以 4 或 9 开头，使用 减法形式，表示从以下符号中减去一个符号，例如 4 是 5 (V) 减 1 (I): IV ，9 是 10 (X) 减 1 (I)：IX。仅使用以下减法形式：4 (IV)，9 (IX)，40 (XL)，90 (XC)，400 (CD) 和 900 (CM)。
只有 10 的次方（I, X, C, M）最多可以连续附加 3 次以代表 10 的倍数。你不能多次附加 5 (V)，50 (L) 或 500 (D)。如果需要将符号附加4次，请使用 减法形式。
给定一个整数，将其转换为罗马数字。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number} num
 * @return {string}
 */
var intToRoman = function (num) {
  const valueSymbols = [
    [1000, "M"],
    [900, "CM"],
    [500, "D"],
    [400, "CD"],
    [100, "C"],
    [90, "XC"],
    [50, "L"],
    [40, "XL"],
    [10, "X"],
    [9, "IX"],
    [5, "V"],
    [4, "IV"],
    [1, "I"]];
  let roman = [];
  for (const [value, symbol] of valueSymbols) {
    while (num >= value) {
      num -= value;
      roman.push(symbol);
    }
    if (num === 0) {
      break;
    }
  }
  return roman.join('');
};`
              }
            ]
          },
          {
            question: `58. 最后一个单词的长度`,
            description: `给你一个字符串 s，由若干单词组成，单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function (s) {
  return s.trim().split(' ').pop().length;
};`
              }
            ]
          },
          {
            question: `14. 最长公共前缀`,
            description: `编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀，返回空字符串 ""。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function (strs) {
  if (strs.length === 0) return '';
  if (strs.length === 1) return strs[0];
  let res = strs[0];
  for (let i = 0; i < strs.length; i++) {
    while (!strs[i].startsWith(res)) {
      res = res.slice(0, res.length - 1)
    }
  }
  return res;
};`
              }
            ]
          },
          {
            question: `151. 反转字符串中的单词`,
            description: `给你一个字符串 s ，请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意：输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中，单词间应当仅用单个空格分隔，且不包含任何额外的空格。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {string}
 */
var reverseWords = function (s) {
  return s.split(' ').filter(i => i).reverse().join(' ');
};`
              }
            ]
          },
          {
            question: `28. 找出字符串中第一个匹配项的下标`,
            description: `给你两个字符串 haystack 和 needle ，请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标（下标从 0 开始）。如果 needle 不是 haystack 的一部分，则返回  -1 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function (haystack, needle) {
  let len = haystack.length;
  let leedlelen = needle.length;
  for (let i = 0; i <= len - leedlelen; i++) {
    if (haystack.startsWith(needle, i)) {
      return i;
    }
  }
  return -1;
};`
              }
            ]
          },
          {
            question: `125. 验证回文串`,
            description: `如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后，短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。

字母和数字都属于字母数字字符。

给你一个字符串 s，如果它是 回文串 ，返回 true ；否则，返回 false 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function (s) {
  // 预处理字符串：转换为小写并移除所有非字母数字字符
  const processed = s.toLowerCase().replace(/[^a-z0-9]/g, '');

  // 比较处理后的字符串与反转后的字符串
  return processed === processed.split('').reverse().join('');
}
`
              }
            ]
          },
          {
            question: `392. 判断子序列`,
            description: `给定字符串 s 和 t ，判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些（也可以不删除）字符而不改变剩余字符相对位置形成的新字符串。（例如，"ace"是"abcde"的一个子序列，而"aec"不是）。

进阶：

如果有大量输入的 S，称作 S1, S2, ... , Sk 其中 k >= 10亿，你需要依次检查它们是否为 T 的子序列。在这种情况下，你会怎样改变代码？

致谢：

特别感谢 @pbrother 添加此问题并且创建所有测试用例。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
// 基础解法，双指针方法
var isSubsequence = function (s, t) {
  let sIndex = 0;
  let tIndex = 0;

  while (sIndex < s.length && tIndex < t.length) {
    if (s[sIndex] === t[tIndex]) {
      sIndex++;
    }
    tIndex++;
  }

  return sIndex === s.length;
};`
              }
            ]
          },
          {
            question: `167. 两数之和 II - 输入有序数组`,
            description: `给你一个下标从 1 开始的整数数组 numbers ，该数组已按 非递减顺序排列  ，请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ，则 1 <= index1 < index2 <= numbers.length 。

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

你可以假设每个输入 只对应唯一的答案 ，而且你 不可以 重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} numbers - 已按非递减顺序排列的整数数组
 * @param {number} target - 目标和
 * @return {number[]} - 满足条件的两个数的下标（从1开始）
 */
var twoSum = function (numbers, target) {
  // 左指针，指向数组起始位置
  let left = 0;
  // 右指针，指向数组末尾位置
  let right = numbers.length - 1;

  // 双指针遍历：左指针必须小于右指针
  while (left < right) {
    // 计算当前两指针所指元素的和
    const sum = numbers[left] + numbers[right];

    // 若和等于目标值，直接返回下标（注意题目下标从1开始）
    if (sum === target) {
      return [left + 1, right + 1];
    }
    // 若和小于目标值，说明需要更大的数，左指针右移
    else if (sum < target) {
      left++;
    }
    // 若和大于目标值，说明需要更小的数，右指针左移
    else {
      right--;
    }
  }

  // 题目保证有解，不会执行到这一步，返回空数组作为占位
  return [];
};`
              }
            ]
          },
          {
            question: `11. 盛最多水的容器`,
            description: `给定一个长度为 n 的整数数组 height 。有 n 条垂线，第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线，使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明：你不能倾斜容器。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height = []) {
  let len = height.length;
  let max = 0;
  for (let i = 0, j = len - 1; i < j;) {
    let minheight = height[i] < height[j] ? height[i++] : height[j--];
    let area = [j - i + 1] * minheight;
    max = Math.max(max, area);
  }
  return max;
};

/**
 * @param {number[]} height
 * @return {number}
 */
// 这种写法比较的好理解
var maxArea = function (height) {
  let left = 0;
  let right = height.length - 1;
  let max = 0;

  while (left < right) {
    // 先计算当前宽度（两指针之间的距离）
    const width = right - left;

    // 确定当前高度（取较短的垂直线）
    // 注意：此时不移动指针，仅比较当前值
    const currentHeight = Math.min(height[left], height[right]);

    // 计算当前面积并更新最大值
    max = Math.max(max, width * currentHeight);

    // 移动较短的垂直线指针（关键逻辑）
    if (height[left] < height[right]) {
      left++;
    } else {
      right--;
    }
  }

  return max;
};`
              }
            ]
          },
          {
            question: `15. 三数之和`,
            description: `给你一个整数数组 nums ，判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ，同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意：答案中不可以包含重复的三元组。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  const result = [];
  if (nums.length < 3) return result;

  // 排序数组
  nums.sort((a, b) => a - b);

  for (let i = 0; i < nums.length - 2; i++) {
    // 跳过重复的第一个元素
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    // 这个取负数是为了实现和思维逻辑是一样的
    const target = -nums[i];

    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[left] + nums[right];

      if (sum === target) {
        result.push([nums[i], nums[left], nums[right]]);

        // 跳过重复的第二个元素
        while (left < right && nums[left] === nums[left + 1]) left++;
        // 跳过重复的第三个元素
        while (left < right && nums[right] === nums[right - 1]) right--;

        // 移动指针
        left++;
        right--;
      } else if (sum < target) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;
};`
              }
            ]
          },
          {
            question: `209. 长度最小的子数组`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function (target, nums) {
  let minLen = Infinity; // 初始化最小长度为无穷大
  let left = 0; // 滑动窗口左边界
  let sum = 0; // 当前窗口的和

  for (let right = 0; right < nums.length; right++) {
    // 扩大窗口，累加元素和
    sum += nums[right];

    // 当窗口和大于等于target时，尝试缩小窗口
    while (sum >= target) {
      // 更新最小长度
      minLen = Math.min(minLen, right - left + 1);
      // 缩小窗口，移除左边界元素
      sum -= nums[left];
      left++;
    }
  }

  // 如果没有找到符合条件的子数组，返回0
  return minLen === Infinity ? 0 : minLen;
};`
              }
            ]
          },
          {
            question: `3. 无重复字符的最长子串`,
            description: `给定一个字符串 s ，请你找出其中不含有重复字符的 最长 子串 的长度。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
  // 记录字符上次出现的位置
  const charMap = new Map();
  let maxLen = 0;
  let left = 0;

  for (let right = 0; right < s.length; right++) {
    const char = s[right];
    // 如果字符重复且上次出现位置在窗口内，更新左边界
    if (charMap.has(char) && charMap.get(char) >= left) {
      left = charMap.get(char) + 1;
    }
    // 更新字符的最新位置
    charMap.set(char, right);
    // 计算当前窗口长度并更新最大长度
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
};`
              }
            ]
          },
          {
            question: `36. 有效的数独`,
            description: `请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ，验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。（请参考示例图）`,
            answer: [
              {
                text: ``,
                daima: `/**
 * 判断一个 9x9 的数独是否有效。只需要验证已填入的数字是否有效，不验证数独是否可解。
 * @param {character[][]} board - 9x9 的数独棋盘，未填充的单元格用 '.' 表示
 * @return {boolean} - 如果数独有效返回 true，否则返回 false
 */
var isValidSudoku = function (board) {
  // 1. 检查每一行是否有效
  // 遍历每一行
  for (let i = 0; i < 9; i++) {
    // 使用 Set 存储当前行中出现的数字，Set 可以自动去重
    const row = new Set();
    // 遍历当前行的每个单元格
    for (let j = 0; j < 9; j++) {
      // 如果单元格为空（'.'），跳过不处理
      if (board[i][j] === '.') continue;
      // 如果当前数字已经在 Set 中，说明该行有重复，数独无效
      if (row.has(board[i][j])) return false;
      // 将当前数字添加到 Set 中
      row.add(board[i][j]);
    }
  }

  // 2. 检查每一列是否有效
  // 遍历每一列
  for (let j = 0; j < 9; j++) {
    // 使用 Set 存储当前列中出现的数字
    const col = new Set();
    // 遍历当前列的每个单元格
    for (let i = 0; i < 9; i++) {
      // 跳过空单元格
      if (board[i][j] === '.') continue;
      // 检查重复
      if (col.has(board[i][j])) return false;
      // 添加数字到 Set
      col.add(board[i][j]);
    }
  }

  // 3. 检查每个 3x3 子网格是否有效
  // 遍历每个 3x3 子网格（共 9 个）
  for (let blockRow = 0; blockRow < 3; blockRow++) {
    for (let blockCol = 0; blockCol < 3; blockCol++) {
      // 使用 Set 存储当前子网格中出现的数字
      const subgrid = new Set();
      // 遍历当前子网格的每个单元格
      for (let i = blockRow * 3; i < blockRow * 3 + 3; i++) {
        for (let j = blockCol * 3; j < blockCol * 3 + 3; j++) {
          // 跳过空单元格
          if (board[i][j] === '.') continue;
          // 检查重复
          if (subgrid.has(board[i][j])) return false;
          // 添加数字到 Set
          subgrid.add(board[i][j]);
        }
      }
    }
  }

  // 如果所有检查都通过，数独有效
  return true;
};`
              }
            ]
          },
          {
            question: `54. 螺旋矩阵`,
            description: `给你一个 m 行 n 列的矩阵 matrix ，请按照 顺时针螺旋顺序 ，返回矩阵中的所有元素。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} matrix
 * @return {number[]}
 */
var spiralOrder = function (matrix) {
  if (matrix.length === 0) return [];

  const result = [];
  let top = 0, bottom = matrix.length - 1;
  let left = 0, right = matrix[0].length - 1;

  while (top <= bottom && left <= right) {
    // 从左到右遍历顶部行
    for (let i = left; i <= right; i++) {
      result.push(matrix[top][i]);
    }
    top++; // 顶部边界下移

    // 从上到下遍历右侧列
    for (let i = top; i <= bottom; i++) {
      result.push(matrix[i][right]);
    }
    right--; // 右侧边界左移

    // 检查是否还有剩余行和列
    if (top > bottom || left > right) break;

    // 从右到左遍历底部行
    for (let i = right; i >= left; i--) {
      result.push(matrix[bottom][i]);
    }
    bottom--; // 底部边界上移

    // 从下到上遍历左侧列
    for (let i = bottom; i >= top; i--) {
      result.push(matrix[i][left]);
    }
    left++; // 左侧边界右移
  }

  return result;
};`
              }
            ]
          },
          {
            question: `48. 旋转图像`,
            description: `给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var rotate = function (matrix) {
  const n = matrix.length;

  // 先进行转置操作（行变列）
  for (let i = 0; i < n; i++) {
    for (let j = i; j < n; j++) {
      // 交换 matrix[i][j] 和 matrix[j][i]
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }

  // 再对每一行进行左右翻转
  for (let i = 0; i < n; i++) {
    matrix[i].reverse();
  }
};`
              }
            ]
          },
          {
            question: `73. 矩阵置零`,
            description: `给定一个 m x n 的矩阵，如果一个元素为 0 ，则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var setZeroes = function (matrix) {
  let m = matrix.length; // 矩阵的行数
  let n = matrix[0].length; // 矩阵的列数

  // 使用Set存储需要置零的行和列
  // mSet存储所有需要置零的行号
  // nSet存储所有需要置零的列号
  let mSet = new Set();
  let nSet = new Set();

  // 第一次遍历：找出所有值为0的元素，记录其所在的行和列
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (matrix[i][j] === 0) {
        mSet.add(i); // 记录行号
        nSet.add(j); // 记录列号
      }
    }
  }

  // 第二次遍历：根据mSet中的行号，将对应行的所有元素置为0
  for (const key of mSet) {
    for (let i = 0; i < n; i++) {
      matrix[key][i] = 0;
    }
  }

  // 第三次遍历：根据nSet中的列号，将对应列的所有元素置为0
  for (const key of nSet) {
    for (let i = 0; i < m; i++) {
      matrix[i][key] = 0;
    }
  }

  // 注意：题目要求不返回任何值，但为了测试方便，这里返回修改后的矩阵
  // 在实际提交时可以移除这行
  return matrix;
};
// 这种虽然是写出来了结果，但是没有满足问题的条件
// 这种写法的时间复杂度是 O(n)
// 空间复杂度是 O(m+n)`
              }
            ]
          },
          {
            question: `289. 生命游戏`,
            description: `根据 百度百科 ， 生命游戏 ，简称为 生命 ，是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n 个格子的面板，每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态： 1 即为 活细胞 （live），或 0 即为 死细胞 （dead）。每个细胞与其八个相邻位置（水平，垂直，对角线）的细胞都遵循以下四条生存定律：

如果活细胞周围八个位置的活细胞数少于两个，则该位置活细胞死亡；
如果活细胞周围八个位置有两个或三个活细胞，则该位置活细胞仍然存活；
如果活细胞周围八个位置有超过三个活细胞，则该位置活细胞死亡；
如果死细胞周围正好有三个活细胞，则该位置死细胞复活；
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的，其中细胞的出生和死亡是 同时 发生的。给你 m x n 网格面板 board 的当前状态，返回下一个状态。

给定当前 board 的状态，更新 board 到下一个状态。

注意 你不需要返回任何东西。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
// 豆包的回答答案
var gameOfLife = function (board) {
  const m = board.length;
  const n = board[0].length;

  // 定义八个方向的偏移量，用于检查周围的细胞
  const directions = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1], [0, 1],
    [1, -1], [1, 0], [1, 1]
  ];

  // 辅助函数：计算指定位置周围的活细胞数量
  function countLiveNeighbors(i, j) {
    let count = 0;
    for (const [dx, dy] of directions) {
      const x = i + dx;
      const y = j + dy;
      // 检查坐标是否合法，并且当前细胞是否为活细胞（1）或曾经是活细胞（2）
      if (x >= 0 && x < m && y >= 0 && y < n) {
        if (board[x][y] === 1 || board[x][y] === 2) {
          count++;
        }
      }
    }
    return count;
  }

  // 第一次遍历：标记每个细胞的下一个状态
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      const liveNeighbors = countLiveNeighbors(i, j);

      if (board[i][j] === 1) {
        // 当前是活细胞
        if (liveNeighbors < 2 || liveNeighbors > 3) {
          // 情况1或3：活细胞死亡，标记为2（1→0）
          board[i][j] = 2;
        }
      } else {
        // 当前是死细胞
        if (liveNeighbors === 3) {
          // 情况4：死细胞复活，标记为3（0→1）
          board[i][j] = 3;
        }
      }
    }
  }

  // 第二次遍历：将标记转换为最终状态
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (board[i][j] === 2) {
        board[i][j] = 0; // 曾经的活细胞现在死亡
      } else if (board[i][j] === 3) {
        board[i][j] = 1; // 曾经的死细胞现在复活
      }
    }
  }
};

/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
// leetcode 上面优秀的答案
var gameOfLife = function (board) {
  const copyBoard = board.map(i => ([...i]));
  const dirs = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
  for (let i = 0; i < copyBoard.length; i++) {
    for (let j = 0; j < copyBoard[0].length; j++) {
      let liveCount = 0;
      for (let dir of dirs) {
        if (copyBoard?.[i + dir[0]]?.[j + dir[1]]) {
          liveCount++;
        }
      }
      if (board[i][j] && (liveCount < 2 || liveCount > 3)) {
        board[i][j] = 0;
      }
      if (board[i][j] === 0 && liveCount === 3) {
        board[i][j] = 1;
      }
    }
  }
};`
              }
            ]
          },
          {
            question: `383. 赎金信`,
            description: `给你两个字符串：ransomNote 和 magazine ，判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以，返回 true ；否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function (ransomNote, magazine) {
  // 如果赎金信长度大于杂志长度，直接返回false
  if (ransomNote.length > magazine.length) return false;

  // 使用数组记录每个字符的出现次数（假设字符为小写字母）
  const charCount = new Array(26).fill(0);

  // 统计杂志中每个字符的出现次数
  for (const char of magazine) {
    charCount[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;
  }

  // 检查赎金信中的字符是否都能在杂志中找到足够的次数
  for (const char of ransomNote) {
    const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
    if (charCount[index] <= 0) {
      return false; // 字符不足
    }
    charCount[index]--; // 使用一个字符
  }

  return true; // 所有字符都足够
};`
              }
            ]
          },
          {
            question: `205. 同构字符串`,
            description: `给定两个字符串 s 和 t ，判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ，那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符，同时不改变字符的顺序。不同字符不能映射到同一个字符上，相同字符只能映射到同一个字符上，字符可以映射到自己本身。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isIsomorphic = function (s, t) {
  if (s.length !== t.length) return false;

  const mapST = new Map(); // s到t的映射
  const mapTS = new Map(); // t到s的映射

  for (let i = 0; i < s.length; i++) {
    const charS = s[i];
    const charT = t[i];

    // 检查s到t的映射
    if (mapST.has(charS)) {
      if (mapST.get(charS) !== charT) return false;
    } else {
      mapST.set(charS, charT);
    }

    // 检查t到s的映射（确保双向唯一）
    if (mapTS.has(charT)) {
      if (mapTS.get(charT) !== charS) return false;
    } else {
      mapTS.set(charT, charS);
    }
  }

  return true;
};`
              }
            ]
          },
          {
            question: `290. 单词规律`,
            description: `给定一种规律 pattern 和一个字符串 s ，判断 s 是否遵循相同的规律。

这里的 遵循 指完全匹配，例如， pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} pattern
 * @param {string} s
 * @return {boolean}
 */
var wordPattern = function (pattern, s) {
  const words = s.split(' ');

  // 如果长度不匹配，直接返回false
  if (pattern.length !== words.length) return false;

  const charToWord = new Map();
  const wordToChar = new Map();

  for (let i = 0; i < pattern.length; i++) {
    const char = pattern[i];
    const word = words[i];

    // 检查字符到单词的映射
    if (charToWord.has(char)) {
      if (charToWord.get(char) !== word) return false;
    } else {
      charToWord.set(char, word);
    }

    // 检查单词到字符的映射（确保双向唯一）
    if (wordToChar.has(word)) {
      if (wordToChar.get(word) !== char) return false;
    } else {
      wordToChar.set(word, char);
    }
  }

  return true;
};`
              }
            ]
          },
          {
            question: `242. 有效的字母异位词`,
            description: `给定两个字符串 s 和 t ，编写一个函数来判断 t 是否是 s 的 字母异位词。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isAnagram = function (s, t) {
  // 如果长度不同，直接返回false
  if (s.length !== t.length) return false;

  // 用于记录每个字符出现的次数
  const count = new Array(26).fill(0);

  // 遍历字符串s，统计每个字符出现的次数
  for (let i = 0; i < s.length; i++) {
    // 这里需要注意的是这个 charCodeAt 的函数的效果是一样的，和前面遇到过的代码
    // 这里可以讲一下这个 charCodeAt 函数的效果，就是一个字符串的转换成对应的数字的函数
    // i 就是需要前面的字符串需要转换的字符的下标，这个 'a'.charCodeAt(0) 就是将下标为 0
    // 的字符转换成数字，也就是将 a 这个字符串转换成对应的数字
    // 之前看到的是 char.charCodeAt(0) 也是这个道理。前面的其实是字符串。
    count[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
  }

  // 遍历字符串t，减少对应字符的计数
  for (let i = 0; i < t.length; i++) {
    const index = t.charCodeAt(i) - 'a'.charCodeAt(0);
    // 如果某个字符的计数已经为0，说明t中该字符过多
    if (count[index] === 0) return false;
    count[index]--;
  }

  // 所有字符计数都应为0，否则返回false
  return count.every(c => c === 0);
};`
              }
            ]
          },
          {
            question: `49. 字母异位词分组`,
            description: `给你一个字符串数组，请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string[]} strs
 * @return {string[][]}
 */
var arr = ["eat", "tea", "tan", "ate", "nat", "bat"];

var groupAnagrams = function (strs) {
  let hashMap = new Map();
  for (let i = 0; i < strs.length; i++) {
    let str = [...strs[i]].sort().join("");
    if (hashMap.has(str)) {
      hashMap.set(str, [...hashMap.get(str), strs[i]]);
    } else {
      hashMap.set(str, [strs[i]]);
    }
  }
  return [...hashMap.values()]
};

console.log(groupAnagrams(arr));
/**
 * @param {string[]} strs
 * @return {string[][]}
 */
// ai 的答案，这个的答案貌似是好一点
var groupAnagrams = function (strs) {
  const map = new Map();

  for (const str of strs) {
    // 将字符串排序后作为键
    const sorted = str.split('').sort().join('');

    // 如果键不存在，初始化一个空数组
    if (!map.has(sorted)) {
      map.set(sorted, []);
    }

    // 将原字符串添加到对应键的数组中
    map.get(sorted).push(str);
  }

  // 将Map的值转换为二维数组返回
  return Array.from(map.values());
};`
              }
            ]
          },
          {
            question: `1. 两数之和`,
            description: `给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。

你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
// 这是 ai 的回答，但是我觉得我的答案好像比较好
// 不对，我发现这 ai 的回答，好像更加的好理解
var twoSum = function (nums, target) {
  const map = new Map(); // 创建哈希表存储已遍历的数值和索引

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    // 检查补数是否已存在于哈希表中
    if (map.has(complement)) {
      return [map.get(complement), i]; // 返回补数的索引和当前索引
    }

    // 将当前数值和索引存入哈希表
    map.set(nums[i], i);
  }

  return []; // 题目保证有解，理论上不会执行到这一步
};`
              }
            ]
          },
          {
            question: `202. 快乐数`,
            description: `编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为：

对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1，也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1，那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ；不是，则返回 false 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function (n) {
  // 使用Set记录已经出现过的数字，用于检测循环
  const seen = new Set();

  // 当数字不等于1且未出现过循环时，继续迭代
  while (n !== 1 && !seen.has(n)) {
    // 将当前数字加入已出现集合
    seen.add(n);
    // 计算下一个数字（各位数字的平方和）
    n = getNext(n);
  }

  // 如果最终数字为1，说明是快乐数；否则是循环
  return n === 1;
};

/**
 * 辅助函数：计算数字n的各位数字的平方和
 * @param {number} n - 输入的数字
 * @return {number} - 各位数字的平方和
 */
function getNext(n) {
  let sum = 0;
  // 逐位处理数字n
  while (n > 0) {
    // 获取最后一位数字
    const digit = n % 10;
    // 累加该位数字的平方
    sum += digit * digit;
    // 移除最后一位数字（向下取整）
    n = Math.floor(n / 10);
  }
  return sum;
}`
              }
            ]
          },
          {
            question: `219. 存在重复元素 II`,
            description: `给你一个整数数组 nums 和一个整数 k ，判断数组中是否存在两个 不同的索引 i 和 j ，满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在，返回 true ；否则，返回 false 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @param {number} k
 * @return {boolean}
 */
var containsNearbyDuplicate = function (nums, k) {
  const map = new Map(); // 存储数字及其最后出现的索引

  for (let i = 0; i < nums.length; i++) {
    const num = nums[i];

    // 如果数字已存在且距离不超过k，返回true
    if (map.has(num) && i - map.get(num) <= k) {
      return true;
    }

    // 更新数字的最后出现位置
    map.set(num, i);
  }

  return false; // 未找到符合条件的重复元素
};`
              }
            ]
          },
          {
            question: `128. 最长连续序列`,
            description: `给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {number}
 */
// 这个是 ai 的回答，我感觉比下面的答案要好一点
// 但是我的答案比较的好理解，因为用的是 sort 方法来进行的排序
// 所以算法的事件复杂度是 O(nlogn)
// 但是 ai 回答的算法的时间复杂度是 O(n)
// 是符合题目的要求的
// 但是写的话，还是推荐使用我的写法，这样比较的简答
var longestConsecutive = function (nums) {
  // 将数组元素存入Set，便于快速查找
  const numSet = new Set(nums);
  let longestStreak = 0;

  // 遍历每个数字
  for (const num of numSet) {
    // 只处理序列的起始数字（即num-1不存在于集合中）
    if (!numSet.has(num - 1)) {
      let currentNum = num;
      let currentStreak = 1;

      // 连续检查后续数字
      while (numSet.has(currentNum + 1)) {
        currentNum += 1;
        currentStreak += 1;
      }

      // 更新最长序列长度
      longestStreak = Math.max(longestStreak, currentStreak);
    }
  }

  return longestStreak;
};`
              }
            ]
          },
          {
            question: `228. 汇总区间`,
            description: `给定一个  无重复元素 的 有序 整数数组 nums 。

区间 [a,b] 是从 a 到 b（包含）的所有整数的集合。

返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说，nums 的每个元素都恰好被某个区间范围所覆盖，并且不存在属于某个区间但不属于 nums 的数字 x 。

列表中的每个区间范围 [a,b] 应该按如下格式输出：

"a->b" ，如果 a != b
"a" ，如果 a == b`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[]} nums
 * @return {string[]}
 */
var summaryRanges = function (nums) {
  if (nums.length === 0) return [];

  const ranges = [];
  let start = nums[0];

  for (let i = 0; i < nums.length; i++) {
    // 如果下一个数不连续，或者已经是最后一个数
    if (i + 1 >= nums.length || nums[i + 1] !== nums[i] + 1) {
      if (start === nums[i]) {
        ranges.push('￥{ start }’);
      } else {
        ranges.push('￥{ start } -> ￥{ nums[i]}');
      }
      // 更新下一个区间的起点
      if (i + 1 < nums.length) {
        start = nums[i + 1];
      }
    }
  }

  return ranges;
};`
              }
            ]
          },
          {
            question: `56. 合并区间`,
            description: `以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间，并返回 一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
// 这是 ai 的回答，我觉得 ai 的回答的比较好
var merge = function (intervals) {
  if (intervals.length === 0) return [];

  // 按区间的起始点升序排序
  intervals.sort((a, b) => a[0] - b[0]);

  const merged = [intervals[0]]; // 初始化结果数组

  for (let i = 1; i < intervals.length; i++) {
    const current = intervals[i];
    const lastMerged = merged[merged.length - 1];

    // 如果当前区间与最后一个合并区间重叠，则合并
    if (current[0] <= lastMerged[1]) {
      // 更新最后一个合并区间的结束点为较大值
      lastMerged[1] = Math.max(lastMerged[1], current[1]);
    } else {
      // 不重叠，直接添加当前区间
      merged.push(current);
    }
  }

  return merged;
};`
              }
            ]
          },
          {
            question: `57. 插入区间`,
            description: `给你一个 无重叠的 ，按照区间起始端点排序的区间列表 intervals，其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束，并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。

在 intervals 中插入区间 newInterval，使得 intervals 依然按照 starti 升序排列，且区间之间不重叠（如果有必要的话，可以合并区间）。

返回插入之后的 intervals。

注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} intervals
 * @param {number[]} newInterval
 * @return {number[][]}
 */
var insert = function (intervals, newInterval) {
  const res = [];
  let i = 0;

  // 1. 添加所有结束点小于新区间起始点的区间（不重叠）
  while (i < intervals.length && intervals[i][1] < newInterval[0]) {
    res.push(intervals[i]);
    i++;
  }

  // 2. 合并所有与新区间重叠的区间
  while (i < intervals.length && intervals[i][0] <= newInterval[1]) {
    newInterval[0] = Math.min(newInterval[0], intervals[i][0]); // 更新合并区间的起点
    newInterval[1] = Math.max(newInterval[1], intervals[i][1]); // 更新合并区间的终点
    i++;
  }
  res.push(newInterval); // 将合并后的新区间加入结果

  // 3. 添加剩余区间（起始点大于新区间终点的区间）
  while (i < intervals.length) {
    res.push(intervals[i]);
    i++;
  }

  return res;
};`
              }
            ]
          },
          {
            question: `452. 用最少数量的箭引爆气球`,
            description: `有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ，其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭，若有一个气球的直径的开始和结束坐标为 xstart，xend， 且满足  xstart ≤ x ≤ xend，则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后，可以无限地前进。

给你一个数组 points ，返回引爆所有气球所必须射出的 最小 弓箭数 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {number[][]} points
 * @return {number}
 */
var findMinArrowShots = function (points) {
  if (points.length === 0) return 0;

  // 按气球结束坐标升序排序
  points.sort((a, b) => a[1] - b[1]);

  let arrows = 1; // 至少需要一支箭
  let end = points[0][1]; // 第一支箭的位置在第一个气球的结束处

  for (let i = 1; i < points.length; i++) {
    // 如果当前气球的开始坐标大于前一个重叠区间的结束坐标，需要新的箭
    if (points[i][0] > end) {
      arrows++;
      end = points[i][1]; // 更新箭的位置到当前气球的结束处
    }
  }

  return arrows;
};`
              }
            ]
          },
          {
            question: `20. 有效的括号`,
            description: `给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。

有效字符串需满足：

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  const stack = [];
  const pairs = {
    ')': '(',
    ']': '[',
    '}': '{'
  };

  for (let char of s) {
    // 如果是右括号
    if (pairs[char]) {
      // 栈为空或栈顶不匹配则无效
      if (stack.length === 0 || stack.pop() !== pairs[char]) {
        return false;
      }
    } else {
      // 如果是左括号，直接入栈
      stack.push(char);
    }
  }

  // 最终栈为空则有效
  return stack.length === 0;
};`
              }
            ]
          },
          {
            question: `71. 简化路径`,
            description: `给你一个字符串 path ，表示指向某一文件或目录的 Unix 风格 绝对路径 （以 '/' 开头），请你将其转化为 更加简洁的规范路径。

在 Unix 风格的文件系统中规则如下：

一个点 '.' 表示当前目录本身。
此外，两个点 '..' 表示将目录切换到上一级（指向父目录）。
任意多个连续的斜杠（即，'//' 或 '///'）都被视为单个斜杠 '/'。
任何其他格式的点（例如，'...' 或 '....'）均被视为有效的文件/目录名称。
返回的 简化路径 必须遵循下述格式：

始终以斜杠 '/' 开头。
两个目录名之间必须只有一个斜杠 '/' 。
最后一个目录名（如果存在）不能 以 '/' 结尾。
此外，路径仅包含从根目录到目标文件或目录的路径上的目录（即，不含 '.' 或 '..'）。
返回简化后得到的 规范路径 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string} path
 * @return {string}
 */
var simplifyPath = function (path) {
  const stack = [];
  const parts = path.split('/');

  for (const part of parts) {
    if (part === '' || part === '.') {
      continue;
    } else if (part === '..') {
      if (stack.length > 0) {
        stack.pop();
      }
    } else {
      stack.push(part);
    }
  }

  return '/' + stack.join('/');
};`
              }
            ]
          },
          {
            question: `155. 最小栈`,
            description: `设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。`,
            answer: [
              {
                text: ``,
                daima: `var MinStack = function () {
  this.stack = [];
  this.minStack = [];
};

/** 
 * @param {number} val
 * @return {void}
 */
MinStack.prototype.push = function (val) {
  this.stack.push(val);
  if (this.minStack.length === 0 || val <= this.minStack[this.minStack.length - 1]) {
    this.minStack.push(val);
  }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function () {
  const val = this.stack.pop();
  if (val === this.minStack[this.minStack.length - 1]) {
    this.minStack.pop();
  }
};

/**
 * @return {number}
 */
MinStack.prototype.top = function () {
  return this.stack[this.stack.length - 1];
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function () {
  return this.minStack[this.minStack.length - 1];
};`
              }
            ]
          },
          {
            question: `150. 逆波兰表达式求值`,
            description: `给你一个字符串数组 tokens ，表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意：

有效的算符为 '+'、'-'、'*' 和 '/' 。
每个操作数（运算对象）都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * @param {string[]} tokens
 * @return {number}
 */
var evalRPN = function (tokens) {
  const stack = [];
  const operators = new Set(['+', '-', '*', '/']);

  for (const token of tokens) {
    if (operators.has(token)) {
      const b = stack.pop();
      const a = stack.pop();

      switch (token) {
        case '+':
          stack.push(a + b);
          break;
        case '-':
          stack.push(a - b);
          break;
        case '*':
          stack.push(a * b);
          break;
        case '/':
          stack.push(Math.trunc(a / b));
          break;
      }
    } else {
      stack.push(Number(token));
    }
  }

  return stack[0];
};`
              }
            ]
          },
          {
            question: `141. 环形链表`,
            description: `给你一个链表的头节点 head ，判断链表中是否有环。

如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。注意：pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ，则返回 true 。 否则，返回 false 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
  let hashSet = new Set();
  while (head !== null) {
    if (hashSet.has(head)) {
      return true;
    }
    hashSet.add(head)
    head = head.next
  }
  return false;
};`
              }
            ]
          },
          {
            question: `2. 两数相加`,
            description: `给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。

请你将两个数相加，并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外，这两个数都不会以 0 开头。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function (l1, l2) {
  const head = new ListNode(0);
  let cur = head;
  let sum = 0;
  while (l1 || l2 || sum) {
    if (l1) {
      sum += l1.val;
      l1 = l1.next;
    }
    if (l2) {
      sum += l2.val;
      l2 = l2.next;
    }
    cur.next = new ListNode(sum % 10);
    cur = cur.next;
    sum = Math.floor(sum / 10);

  }
  return head.next;
};`
              }
            ]
          },
          {
            question: `21. 合并两个有序链表`,
            description: `将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 `,
            answer: [
              {
                text: ``,
                daima: `/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} list1
 * @param {ListNode} list2
 * @return {ListNode}
 */
// ai 的回答也是这样的，而且答案是一模一样的
var mergeTwoLists = function (list1, list2) {
  const head = { val: -1, next: null };
  let end = head;
  while (list1 && list2) {
    if (list1.val < list2.val) {
      end.next = list1;
      list1 = list1.next;
    } else {
      end.next = list2;
      list2 = list2.next;
    }
    end = end.next;
  }
  end.next = list1 || list2;
  return head.next;
};`
              }
            ]
          },
          {
            question: `138. 随机链表的复制`,
            description: `给你一个长度为 n 的链表，每个节点包含一个额外增加的随机指针 random ，该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成，其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如，如果原链表中有 X 和 Y 两个节点，其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ，同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示：

val：一个表示 Node.val 的整数。
random_index：随机指针指向的节点索引（范围从 0 到 n-1）；如果不指向任何节点，则为  null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * // Definition for a Node.
 * function Node(val, next, random) {
 *    this.val = val;
 *    this.next = next;
 *    this.random = random;
 * };
 */

/**
 * @param {Node} head
 * @return {Node}
 */
var copyRandomList = function (head) {
  if (!head) return null;

  // 第一步：复制每个节点并插入原节点之后
  let current = head;
  while (current) {
    const copy = new Node(current.val);
    copy.next = current.next;
    current.next = copy;
    current = copy.next;
  }

  // 第二步：设置复制节点的 random 指针
  current = head;
  while (current) {
    if (current.random) {
      current.next.random = current.random.next;
    }
    current = current.next.next;
  }

  // 第三步：拆分原链表和复制链表
  const dummy = new Node(0);
  let copyCurrent = dummy;
  current = head;

  while (current) {
    copyCurrent.next = current.next;
    current.next = current.next.next;
    copyCurrent = copyCurrent.next;
    current = current.next;
  }

  return dummy.next;
};

/* 
这段代码通过「三次遍历」实现了随机链表的深拷贝，核心思路是利用原链表的节点关系暂存复制节点，避免使用哈希表额外存储映射关系，空间复杂度优化到 O (1)。以下是分步解析：
1. 核心问题
随机链表的深拷贝难点在于：
新节点的 next 指针需要指向复制链表的节点（而非原链表）。
新节点的 random 指针需要指向复制链表中对应的新节点（而非原链表）。
2. 代码分步解析
第一步：复制节点并插入原节点之后
javascript
let current = head;
while (current) {
  const copy = new Node(current.val); // 创建当前节点的复制节点
  copy.next = current.next; // 复制节点的next指向原节点的next
  current.next = copy; // 原节点的next指向复制节点
  current = copy.next; // 移动到下一个原节点（跳过复制节点）
}
效果：
原链表：A → B → C
处理后：A → A' → B → B' → C → C'
（A' 表示 A 的复制节点）
第二步：设置复制节点的 random 指针
javascript
current = head;
while (current) {
  if (current.random) {
    // 原节点A的random指向X，则A'的random指向X'
    current.next.random = current.random.next; 
  }
  current = current.next.next; // 跳过复制节点，移动到下一个原节点
}
关键逻辑：
若原节点 A.random = X，则复制节点 A'.random 应指向 X 的复制节点 X'。
由于第一步已将 X' 插入 X 之后（即 X.next = X'），因此 A'.random = X.next。
第三步：拆分原链表和复制链表
javascript
const dummy = new Node(0); // 复制链表的虚拟头节点
let copyCurrent = dummy;
current = head;

while (current) {
  copyCurrent.next = current.next; // 复制节点加入新链表
  current.next = current.next.next; // 原节点跳过复制节点，恢复原链表
  copyCurrent = copyCurrent.next; // 移动复制链表指针
  current = current.next; // 移动原链表指针
}


效果：
原链表恢复：A → B → C
复制链表生成：A' → B' → C'
3. 最终返回
javascript
return dummy.next; // 复制链表的头节点（跳过虚拟头节点）

4. 优势总结
空间效率：无需哈希表存储原节点与复制节点的映射，空间复杂度 O (1)。
时间效率：三次遍历链表，时间复杂度 O (n)。
通过「插入 - 设置 - 拆分」三步，巧妙利用原链表的结构完成复制，是随机链表深拷贝的经典解法。
*/`
              }
            ]
          },
          {
            question: `92. 反转链表 II`,
            description: `给你单链表的头指针 head 和两个整数 left 和 right ，其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点，返回 反转后的链表 。`,
            answer: [
              {
                text: ``,
                daima: `/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} left
 * @param {number} right
 * @return {ListNode}
 */
var reverseBetween = function (head, left, right) {
  if (!head || left === right) return head;

  const dummy = new ListNode(0);
  dummy.next = head;
  let prev = dummy;

  // 移动prev到left-1位置
  for (let i = 0; i < left - 1; i++) {
    prev = prev.next;
  }

  // 反转区间内的节点
  let current = prev.next;
  for (let i = 0; i < right - left; i++) {
    const next = current.next;
    current.next = next.next;
    next.next = prev.next;
    prev.next = next;
  }

  return dummy.next;
};`
              }
            ]
          },
        ]
      },
      {
        tag: `css面试题`,
        data: [
          {
            question: `1.说说你对盒子模型的理解`,
            description: ``,
            answer: [
              {
                text: `1.1. 是什么
当对一个文档进行布局(Iayout)的时候，浏览器的渲染引|擎会根据标准之一的CSS基础框盒模型
(CSS basic box model)，将所有元素表示为一个个矩形的盒子(box)
一个盒子由四个部分组成：content、padding、border、margin
1.2w3c标准盒子模型
从上图可以看到:
● 盒子总宽度 = width + padding + border + margin;
● 盒子总高度 = height + padding + border + margin
也就是，width/height只是内容高度，不包含padding和border值
所以上面问题中，设置width为200px,但由于存在padding，但实际上盒子的宽度有240px
1.3IE怪异盒子模型
从上图可以看到:
· 盒子总宽度 = width + margin;
· 盒子总高度 = height + margin;
也就是，width/height包含了padding和border值`,
                daima: ``,
              }
            ]
          },
          {
            question: `2.谈谈你对BFC的理解`,
            description: ``,
            answer: [
              {
                text: `和这个BFC相关的我只知道这个外边距塌陷，其他的我没有太了解`,
                daima: ``
              },
              {
                text: `清除内部浮动`,
                daima: ``
              },
              {
                text: `这一方面在面试的时候问的比较少，所以就没有太过于了解`,
                daima: ``
              }
            ]
          },
          {
            question: `3.什么是响应式设计？响应式设计的基本原理是什么？如何做?`,
            description: ``,
            answer: [
              {
                text: `3.1.是什么
响应式网站设计(ResponsiveWebdesign)是一种网络页面设计布局，页面的设计与开发应当根据用
户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整
描述响应式界面最著名的一句话就是“Content is like water”
大白话便是“如果将屏幕看作容器，那么内容就像水一样”
响应式网站常见特点：
● 同时适配PC +平板+手机等
●标签导航在接近手持终端设备时改变为经典的抽屉式导航
◆网站的布局会根据视口来调整模块的大小和位置`,
                daima: ``
              },
              {
                text: `媒体查询
                CSS3中的增加了更多的媒体查询，就像if条件表达式一样，我们可以设置不同类型的媒体条件，并
根据对应的条件，给相应符合条件的媒体调用相对应的样式表
使用@Media查询，可以针对不同的媒体类型定义不同的样式，如:`,
                daima: `@media screen and (max-width: 1920px){... }`,
              },
              {
                text: `当视口在375px-600px之间，设置特定字体大小18px`,
                daima: `@media screen (min-width: 375px) and (max-width: 600px){
body {
font-size: 18px;
}
}`,
              },
              {
                text: `通过媒体查询，可以通过给不同分辨率的设备编写不同的样式来实现响应式的布局，比如我们为不同分
辨率的屏幕，设置不同的背景图片
比如给小屏幕手机设置@2x图，为大屏幕手机设置@3x图，通过媒体查询就能很方便的实现`,
                daima: ``,
              },
              {
                text: `百分比实现页面的响应式`,
                daima: ``,
              },
              {
                text: `使用vw/wh的方式来实现页面的响应式`,
                daima: ``,
              },
              {
                text: `rem的方式来实现页面的响应式`,
                daima: ``,
              }
            ]
          },
          {
            question: `元素水平垂直居中的方法有哪些？如果元素不定宽高呢?`,
            description: `在开发中经常遇到这个问题，即让某个元素的内容在水平和垂直方向上都居中，内容不仅限于文字，可
能是图片或其他元素
居中是一个非常基础但又是非常重要的应用场景，实现居中的方法存在很多，可以将这些方法分成两个
大类：
●居中元素（子元素）的宽高已知
，居中元素宽高未知`,
            answer: [
              {
                text: `4.2.1. 利用定位+margin:auto`,
                daima: `<style>
    .father{
        width:500px;
        height:300px;
        border:1px solid #0a3b98;
        position: relative;
    }
    .son{
        width:100px;
        height:40px;
        background: #f0a238;
        position: absolute;
        top:0;
        left:0;
        right:0;
        bottom:0;
        margin:auto;
    }
 </style>
 <div class="father">
    <div class="son"></div>
 </div>`
              },
              {
                text: `4.2.2. 利用定位+margin:负值`,
                daima: `<style>
    .father {
 position: relative;
 width: 200px;
 height: 200px;
 background: skyblue;
 }
    .son {
 position: absolute;
 top: 50%;
 left: 50%;
 margin-left:-50px;
 margin-top:-50px;
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>
 <div class="father">
 <div class="son"></div>
 </div>`,
              },
              {
                text: `4.2.3.利用定位+transform`,
                daima: `<style>
    .father {
 position: relative;
 width: 200px;
 height: 200px;
 background: skyblue;
 }
    .son {
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%,-50%);
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>
 <div class="father">
    <div class="son"></div>
 </div>`,
              },
              {
                text: `4.2.4. table布局`,
                daima: `<style>
    .father {
 display: table-cell;
 width: 200px;
 height: 200px;
 background: skyblue;
 vertical-align: middle;
 text-align: center;
 }
    .son {
 display: inline-block;
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>
 <div class="father">
 <div class="son"></div>
 </div>`,
              },
              {
                text: `4.2.5. flex弹性布局`,
                daima: `<style>
    .father {
 display: flex;
 justify-content: center;
 align-items: center;
 width: 200px;
 height: 200px;
 background: skyblue;
 }
    .son {
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>
 <div class="father">
 <div class="son"></div>
 </div>`,
              },
              {
                text: `4.2.6. grid网格布局`,
                daima: `<style>
    .father {
 display: grid;
 align-items:center;
 justify-content: center;
 width: 200px;
 height: 200px;
 background: skyblue;
 }
        .son 
}
 {
 width: 10px;
 height: 10px;
 border: 1px solid red
 </style>
 <div class="father">
 <div class="son"></div>
 </div>`,
              }
            ]
          },
          {
            question: `5.如何实现两栏布局，右侧自适应？三栏布局中间自适应呢？`,
            description: ``,
            answer: [
              {
                text: `5.1.1. 两栏布局
两栏布局实现效果就是将页面分割成左右宽度不等的两列，宽度较小的列设置为固定宽度，剩余宽度由
另一列撑满，
比如AntDesign文档，蓝色区域为主要内容布局容器，侧边栏为次要内容布局容器`,
                daima: ``
              },
              {
                text: `5.1.2. 三栏布局
三栏布局按照左中右的顺序进行排列，通常中间列最宽，左右两列次之
大家最常见的就是github：`,
                daima: ``,
              },
              {
                text: `5.2.两栏布局
两栏布局非常常见，往往是以一个定宽栏和一个自适应的栏并排展示存在
实现思路也非常的简单：
使用float左浮左边栏
●右边模块使用margin-left撑出内容块做内容展示
· 为父级元素添加BFC，防止下方元素飞到上方内容
代码如下：`,
                daima: `<style>
    .box{
 overflow: hidden; BFC
 }
    .left {
 float: left;
 width: 200px;
 background-color: gray;
 height: 400px;
 }
    .right {
 margin-left: 210px;
 background-color: lightgray;
 height: 200px;
 }
 </style>
 <div class="box">
 <div class="left">
 </div>`,
              },
              {
                text: `还有一种更为简单的使用则是采取：flex弹性布局
5.2.1. flex弹性布局`,
                daima: `<style>
    .box{
 display: flex;
 }
    .left {
 width: 100px;
 }
    .right {
 flex: 1;
 }
 </style>
 <div class="box">
 <div class="left"> </div>
 <div class="right"> </div>
 </div>`,
              },
              {
                text: `flex可以说是最好的方案了，代码少，使用简单
注意的是，flex容器的一个默认属性值:align-items：stretch;
这个属性导致了列等高的效果。为了让两个盒子高度自动，需要设置：align-items：flex-start`,
                daima: ``,
              },
              {
                text: `5.3.三栏布局
实现三栏布局中间自适应的布局方式有：
● 两边使用 float,中间使用 margin
codeStUc
●两边使用 absolute，中间使用margin
● 两边使用 float 和负 margin
印客学院
●display: table 实现
·flex实现
●grid网格布局
5.3.1. 两边使用 float,中间使用 margin
需要将中间的内容放在html结构最后，否则右侧会臣在中间内容的下方
实现代码如下：`,
                daima: `<style>
    .wrap {
        background: #eee;
        overflow: hidden; <!-- BFC -->
        padding: 20px;
        height: 200px;
    }
    .left {
        width: 200px;
        height: 200px;
        float: left;
        background: coral;
    }
    .right {
        width: 120px;
        height: 200px;
        float: right;
        background: lightblue;
    }
    .middle {
        margin-left: 220px;
        height: 200px;
        background: lightpink;
        margin-right: 140px;
    }
 </style>
 <div class="wrap">
    <div class="left"> </div>
    <div class="right"> </div>
    <div class="middle"> </div>
 </div>`,
              },
              {
                text: `原理如下：
●两边固定宽度，中间宽度自适应。
●利用中间元素的margin值控制两边的间距
●宽度小于左右部分宽度之和时，右侧部分会被挤下去
这种实现方式存在缺陷：
◆主体内容是最后加载的。
●右边在主体内容之前，如果是响应式设计，不能简单的换行展示
5.3.2. 两边使用 absolute，中间使用 margin
基于绝对定位的三栏布局：注意绝对定位的元素脱离文档流，相对于最近的已经定位的祖先元素进行定
位。无需考虑HTML中结构的顺序`,
                daima: `<style>
  .container {
 position: relative;
 }
  .left,
  .right,
  .main {
 height: 200px;
 line-height: 200px;
 text-align: center;
 }
  .left {
 position: absolute;
 top: 0;
 left: 0;
 width: 100px;
 background: green;
 }
  .right {
 position: absolute;
 top: 0;
 right: 0;
 width: 100px;
 background: green;
 }
  .main {
 margin: 0 110px;
 background: black;
 color: white;
 }
 </style>
 <div class="container">
 <div class="left"> </div>
 <div class="right"> </div>
 <div class="main">
 </div>`,
              },
              {
                text: `实现流程：
·左右两边使用绝对定位，固定在两侧。
●中间占满一行，但通过margin和左右两边留出10px的间隔
5.3.3. 两边使用 float 和负 margin`,
                daima: `<style>
  .left,
  .right,
  .main {
 height: 200px;
 line-height: 200px;
 text-align: center;
 }
  .main-wrapper {
 float: left;
 width: 100%;
 }
  .main {
 margin: 0 110px;
 background: black;
 color: white;
 }
  .left,
  .right {
 float: left;
 width: 100px;
 margin-left: -100%;
 background: green;
 }
 }
 </style>
  .right {
 margin-left: -100px; /*  */
 <div class="main-wrapper">
 <div class="main"> </div>
 </div>
 <div class="left"> </div>
 <div class="right"> </div>`,
              },
              {
                text: `·中间使用了双层标签，外层是浮动的，以便左中右能在同一行展示
◆左边通过使用负margin-left:-100%，相当于中间的宽度，所以向上偏移到左侧
·右边通过使用负 margin-left:-100px,相当于自身宽度,所以向上偏移到最右侧
缺点：
●增加了.main-wrapper 一层，结构变复杂
●使用负 margin，调试也相对麻烦
院
deStudio
5.3.4. 使用 display: table实现
<table>标签用于展示行列数据，不适合用于布局。但是可以使用display：table来实现布局
的效果`,
                daima: `<style>
  .container {
    height: 200px;
    line-height: 200px;
    text-align: center;
    display: table;
    table-layout: fixed;
    width: 100%;
  }
 
  .left,
  .right,
  .main {
    display: table-cell;
  }
 
  .left,
  .right {
    width: 100px;
    background: green;
  }
 
  .main {
    background: black;
    color: white;
    width: 100%;
  }
 </style>
 
<div class="container">
  <div class="left"> </div>
  <div class="main"> </div>
  <div class="right"> </div>
 </div>`,
              },
              {
                text: `实现原理：
●层通过display: table设置为表格，设置table-layout: fixed表示列宽自身宽度决定，而不是自动计
算。
● 内层的左中右通过 display: table - cell设置为表格单元。
●左右设置固定宽度，中间设置 width: 100 % 填充剩下的宽度
                  5.3.5.使用flex实现
                  利用flex弹性布局，可以简单实现中间自适应
代码如下：`,
                daima: `<style type="text/css">
    .wrap {
        display: flex;
        justify-content: space-between;
    }
 
    .left,
    .right,
    .middle {
        height: 100px;
    }
 
    .left {
        width: 200px;
        background: coral;
    }
 
    .right {
        width: 120px;
        background: lightblue;
    }
 
    .middle {
        background: #555;
        width: 100%;
        margin: 0 20px;
    }
 </style>
 <div class="wrap">
    <div class="left"> </div>
    <div class="middle"> </div>
    <div class="right"> </div>
 </div>`,
              },
              {
                text: `实现过程：
● 仅需将容器设置为display:flex;，
●盒内元素两端对其，将中间元素设置为100%宽度，或者设为flex:1，即可填充空白
●盒内元素的高度撑开容器的高度
优点：
●结构简单直观
·可以结合 fex的其他功能实现更多效果，例如使用order属性调整显示顺序，让主体内容优先加载，
但展示在中间
5.3.6. grid网格布局
deStudi
代码如下：`,
                daima: `<style>
    .wrap {
        display: grid;
        width: 100%;
        grid-template-columns: 300px auto 300px;
    }
 
    .left,
    .right,
    .middle {
        height: 100px;
    }
 
    .left {
        background: coral;
    }
 
    .right {
        background: lightblue;
    }
 
    .middle {
        background: #555;
    }
 </style>
 <div class="wrap">
    <div class="left"> </div>
    <div class="middle"> </div>
    <div class="right"> </div>
 </div>`,
              }
            ]
          },
          {
            question: `6.css选择器有哪些?优先级?哪些属性可以继承?`,
            description: ``,
            answer: [
              {
                text: `关于css属性选择器常用的有:
● id选择器(#box)，选择id为box的元素
●类选择器(.one)，选择类名为one的所有元素
●标签选择器(div)，选择标签为div的所有元素
●后代选择器(#box div)，选择id为box元素内部所有的div元素
◆子选择器（.one>one_1)，选择父元素为.one的所有.one_1的元素
● 相邻同胞选择器（.one+.two)，选择紧接在.one之后的所有.two元素
●群组选择器(div,p)，选择div、p的所有元素
还有一些使用频率相对没那么多的选择器：
·伪类选择器`,
                daima: `：1ink：选择未被访问的链接
:visited：选取已被访问的链接
:active：选择活动链接
:hover：鼠标指针浮动在上面的元素
:focus：选择具有焦点的
:first-child：父元素的首个子元素
●伪元素选择器
:first-letter：用于选取指定选择器的首字母
:first-line：选取指定选择器的首行
:before：选择器在被选元素的内容前面插入内容
:after：选择器在被选元素的内容后面插入内容
属性选择器
[attribute]选择带有attribute属性的元素
[attribute=value]选择所有使用attribute=value的元素
[attribute~=value]选择attribute属性包含value的元素
[attribute|=value]：选择attribute属性以value开头的元素`
              },
              {
                text: `在CSS3中新增的选择器有如下：
●层次选择器(p~ul)，选择前面有p元素的每个ul元素
◆伪类选择器`,
                daima: `:first-of-type表示一组同级元素中其类型的第一个元素
：last-of-type表示一组同级元素中其类型的最后一个元素
: only-of-type 表示没有同类型兄弟元素的元素
:only-child表示没有任何兄弟的元素
:nth-child（n）根据元素在一组同级中的位置匹配元素
:nth-last-of-type（n）匹配给定类型的元素，基于它们在一组兄弟元素中的位置，从末尾开始计
数
:last-child表示一组兄弟元素中的最后一个元素
:root设置HTML文档
:empty指定空的元素
:enabled选择可用元素
: disabled选择被禁用元素
：checked选择选中的元素
:not(selector）选择与<selector>不匹配的所有元素
  属性选择器
[attribute*=value]：选择attribute属性值包含value的所有元素
[attribute^=value]：选择attribute属性开头为value的所有元素
[attribute$=value]：选择attribute属性结尾为value的所有元素`,
              },
              {
                text: `相信大家对CSS选择器的优先级都不陌生：
内联＞ID选择器＞类选择器＞标签选择器`,
                daima: ``,
              },
              {
                text: `6.3.继承属性
在css中，继承是指的是给父元素设置一些属性，后代元素会自动拥有这些属性
关于继承属性，可以分成：`,
                daima: `·字体系列属性
font:组合字体
font-family: 规定元素的字体系列
font-weight:设置字体的粗细
font-size:设置字体的尺寸
font-style:定义字体的风格
font-variant:偏大或偏小的字体
●文本系列属性
text-indent：文本缩进
text-align:文本水平对刘
line-height:行高
word-spacing：增加或减少单词间的空白
letter-spacing:增加或减少字符间的空白
text-transform:控制文本大小写
direction：规定文本的书写方向
color：文本颜色`,
              }
            ]
          },
          {
            question: `7. cSs中，有哪些方式可以隐藏页面元素?区别?`,
            description: ``,
            answer: [
              {
                text: `7.2.实现方式
通过css实现隐藏元素方法有如下：
display:none
● visibility:hidden
● opacity:0
Encodes
●设置height、width模型属性为0
印客
●position:absolute
● clip-path
7.2.1. display:none
设置元素的display为none是最常用的隐藏元素的方法
将元素设置为display:none后，元素在页面上将彻底消失
元素本身占有的空间就会被其他元素占有，也就是说它会导致浏览器的重排和重绘
消失后，自身绑定的事件不会触发，也不会有过渡效果
特点：元素不可见，不占据空间，无法响应点击事件
7.2.2.visibility:hidden
设置元素的visibility为hidden也是一种常用的隐藏元素的方法
从页面上仅仅是隐藏该元素，DOM结果均会存在，只是当时在一个不可见的状态，不会触发重排，但是
会触发重绘
给人的效果是隐藏了，所以他自身的事件不会触发
特点：元素不可见，占据页面空间，无法响应点击事件
学院
7.2.3. opacity:0
opacity属性表示元素的透明度，将元素的透明度设置为0后，在我们用户眼中，元素也是隐藏的
不会引发重排，一般情况下也会引发重绘
如果利用animation 动画，对opacity做变化(animation会默认触发GPU加速》，则只会触发GPU
层面的 composite，不会触发重绘
由于其仍然是存在于页面上的，所以他自身的的事件仍然是可以触发的，但被他遮挡的元素是不能触发
其事件的
需要注意的是：其子元素不能设置opacity来达到显示的效果
院
特点：改变元素透明度，元素不可见，占据页面空间，可以响应点击事件
7.2.4. 设置height、width属性为0
将元素的margin，border，padding，height和width等影响元素盒模型的属性设置成
0，如果元素内有子元素或内容，还应该设置其overflow:hidden来隐藏其子元素
特点：元素不可见，不占据页面空间，无法响应点击事件
E印P气
7.2.5. position:absolute
将元素移出可视区域
特点：元素不可见，不影响页面布局
7.2.6.clip-path
通过裁剪的形式`,
                daima: ``
              }
            ]
          },
          {
            question: `8.如何实现单行/多行文本溢出的省略样式?`,
            description: ``,
            answer: [
              {
                text: `8.2.实现方式
8.2.1.单行文本溢出省略
udiO
院
理解也很简单，即文本在一行内显示，超出部分以省略号的形式展现
实现方式也很简单，涉及的css属性有：
●text-overflow：规定当文本溢出时，显示省略符号来代表被修剪的文本
white-space：设置文字在一行显示，不能换行
overflow：文字长度超出限定宽度，则隐藏超出的内容
overflow设为hidden，普通情况用在块级元素的外层隐藏内部溢出元素，或者配合下面两个属性
实现文本溢出省略
white-space:nowrap，作用是设置文本不换行，是overflow:hidden和text-overflow:
ellipsis生效的基础
text-overflow属性值有如下：
●clip：当对象内文本溢出部分裁切掉
● ellipsis：当对象内文本溢出时显示省略标记（...）
text-overflow只有在设置了overflow:hidden和white-space:nowrap才能够生效的`,
                daima: ``
              }
            ]
          },
          {
            question: `11.CSS3新增了哪些新特性？`,
            description: ``,
            answer: [
              {
                text: `11.3.新样式
11.3.1. 边框
css3新增了三个边框属性，分别是：
● border-radius:创建圆角边框
●box-shadow：为元素添加阴影
·border-image：使用图片来绘制边框
11.3.1.1. box-shadow
设置元素阴影，设置属性如下：
·水平阴影
·垂直阴影
●模糊距离(虚实)
●阴影尺寸(影子大小)
EncodeStudio
。阴影颜色
·内/外阴影
印客学院
其中水平阴影和垂直阴影是必须设置的
11.3.2. 背景
新增了几个关于背景的属性，分别是background-clip、background-origin、backgroun
d-size和background-break
11.3.2.1. background-clip
用于确定背景画区，有以下几种可能的属性：
● background-clip: border-box; 背景从border开始显示
● background-clip: padding-box;背景从padding开始显示
学院
●background-clip: content-box;背景显content区域开始显示
● background-clip: no-clip; 默认属性,等同于border-box
通常情况，背景都是覆盖整个元素的，利用这个属性可以设定背景颜色或图片的覆盖范围
11.3.2.2. background-origin
当我们设置背景图片时，图片是会以左上角对齐，但是是以border的左上角对齐还是
以padding的左上角或者content的左上角对齐?border-origin正是用来设置这个的
● background-origin: border-box; 从border开始计算background-position
background-origin: padding-box;从padding开始计算background-position
● background-origin: content-box;从content开始计算background-position
默认情况是padding-box，即以padding的左上角为原点
11.3.2.3. background-size
background-size属性常用来调整背景图片的大小，主要用于设定图片本身。有以下可能的属性：
●background-size: contain;缩小图片以适合元素(维持像素长宽比)
● background-size: cover;扩展元素以填补元素(维持像素长宽比)
● background-size: 100px 100px;缩小图片至指定的大小
● background-size:50%100%;缩小图片至指定的大小，百分比是相对包含元素的尺寸
11.3.3. background-break
元素可以被分成几个独立的盒子(如使内联元素span跨越多行），background-break属性用来控
制背景怎样在这些不同的盒子中显示
●background-break:continuous;默认值。忽略盒之间的距离（也就是像元素没有分成多个盒子，
依然是一个整体一样）
● background-break: bounding-box; 把盒之间的距离计算在内;
◆ background-break:each-box;为每个盒子单独重绘背景
11.3.4. 文字
11.3.5.word-wrap
语法:word-wrap: normal|break-word
●normal：使用浏览器默认的换行
●break-all:允许在单词内换行
11.3.6. text-overflow
text-overflow设置或检索当当前行超过指定容器的边界时如何显示，属性有两个值选择：
· clip：修剪文本
●ellipsis：显示省略符号来代表被修剪的文本
11.3.7.text-shadow
text-shadow可向文本应用阴影。能够规定水平阴影、垂直阴影、模糊距离，以及阴影的颜色
11.3.8. text-decoration
CSS3里面开始支持对文字的更深层次的渲染，具体有三个属性可供设置：
●text-fill-color:设置文字内部填充颜色
● text-stroke-color:设置文字边界填充颜色
●text-stroke-width:设置文字边界宽度
11.3.9.颜色
codeStudio
css3新增了新的颜色表示方式rgba与hsla
客学院
· rgba分为两部分，rgb为颜色值，a为透明度
●hala分为四部分，h为色相，s为饱和度，I为亮度，a为透明度
11.4. transition过渡
transition属性可以被指定为一个或多个CSS属性的过渡效果，多个属性之间用逗号进行分隔，
必须规定两项内容：
·过度效果
·持续时间
语法如下：`,
                daima: `transtion: CSS属性，花费时间，效果曲线（默认ease），延迟时间（默认0）`
              },
              {
                text: `上面为简写模式，也可以分开写各个属性`,
                daima: `transition-property: width;
transition-duration: 1s;
transition-timing-function: linear;
transition-delay: 2s;`
              },
              {
                text: `11.4.1. transform 转换
transform属性允许你旋转，缩放，倾斜或平移给定元素
transform-origin：转换元素的位置(围绕那个点进行转换)，默认值为(x,y,z):(50%,50%,
0)
使用方式:
● transform: translate(120px, 50%):位移
● transform: scale(2, 0.5)： 缩放
● transform: rotate(0.5turn):旋转
● transform: skew(30deg, 20deg):倾斜
>deStudio
学院
11.4.2. animation 动画
动画这个平常用的也很多，主要是做一个预设的动画。和一些页面交互的动画效果，结果和过渡应该一
样，让页面不会那么生硬
animation也有很多的属性
● animation-name:动画名称
●animation-duration：动画持续时间
● animation-timing-function:动画时间函数
● animation-delay:动画延迟时间
●animation-iteration-count:动画执行次数,可以设置为一个整数,也可以设置为infinite，意思是
无限循环
●animation-direction:动画执行方向
●animation-paly-state:动画播放状态
●animation-fill-mode：动画填充模式
11.5.渐变
颜色渐变是指在两个颜色之间平稳的过渡，
css3渐变包括
· linear-gradient:线性渐变
｜background-image: linear-gradient(direction,color-stop1, color-stop2,..;
●radial-gradient：径向渐变
linear-gradient(odeg, red, green);
11.6. 其他
关于css3其他的新特性还包括flex弹性布局、Grid栅格布局，这两个布局在以前就已经讲过，
这里就不再展示
除此之外，还包括多列布局、媒体查询、混合模式等等……`,
                daima: ``,
              }
            ]
          },
          {
            question: `12. css3动画有哪些?`,
            description: ``,
            answer: [
              {
                text: `12.2.1. transition实现渐变动画
transition的属性如下：
● property:填写需要变化的css属性
● duration:完成过渡效果需要的时间单位(s或者ms)
●timing-function:完成效果的速度曲线
●delay: 动画效果的延迟触发时间
12.2.2. transform转变动画
cO
包含四个常用的功能：
translate：位移
印客字！
● scale：缩放
●rotate：旋转
● skew:倾斜
一般配合transition过度使用
注意的是，transform不支持inline元素，使用前把它变成block
12.2.3. animation实现自定义动画
然后这个表格的实现在这里不是特别的好，最好是自己到网上去找找对应的表格`,
                daima: ``
              }
            ]
          },
          {
            question: `13.介绍一下grid网格布局`,
            description: `这个的答案最好是自己去找找，对应的资料。这里由于太多需要实现的效果太过于复杂，就不展现了`,
            answer: [
              {
                text: ``,
                daima: ``
              }
            ]
          },
          {
            question: `14.说说flexbox（弹性盒布局模型），以及适用场景?`,
            description: `这个的答案由于自己已经非常的熟悉了，所以就展现的比较的简单，如果需要具体的内容可以自己去网上面找找
            网上面的资料更加的全面，但是需要自己去花时间，从一个页面跳转到另一个页面，这个的跨度是比较的大的，但是这是没有办法的事情`,
            answer: [
              {
                text: `14.2. 属性
关于flex常用的属性，我们可以划分为容器属性和容器成员属性
容器属性有：
● flex-direction
Encod
·flex-wrap
. flex-flow
印客学
●justify-content
align-items
● align-content`,
                daima: ``
              }
            ]
          },
          {
            question: `15.说说设备像素、css像素、设备独立像素、dpr、ppi之间的区别?`,
            description: `这个也是不在这里直接放答案了，这个答案需要自己去网上面找找，因为这里的答案比较的复杂，需要实现的效果比较的复杂`,
            answer: [
              {
                text: ``,
                daima: ``
              }
            ]
          },
          {
            question: `16.说说em/px/rem/vh/vw区别?`,
            description: `这个也是比较的简单，自己也是非常的熟悉这些的区别了，就是按照自己想的一样直接说出来就行了。
            但是这个px这个东西可以和刚刚的说说设备的像素，css像素，设备独立像素，dpr，ppi之间的区别向比较`,
            answer: [
              {
                text: ``,
                daima: ``
              }
            ]
          },
          {
            question: `17. 让Chrome支持小于12px的文字方式有哪些?区别？`,
            description: ``,
            answer: [
              {
                text: `常见的解决方案有：
● zoom
●-webkit-transform:scale()
◆-webkit-text-size-adjust:none
这里就说一下这个zoom这个方案就行了，对于其他的两种就说一下自己知道但是没有记住就行了`,
                daima: ``
              }
            ]
          },
          {
            question: `18.怎么理解回流跟重绘？什么场景下会触发？`,
            description: `这个的答案的展现也是比较的复杂，对应的资料可以自己去网上面找找，但是需要注意的是，可以结合浏览器的原理来理解这个回流和重绘，这样的效果会比较的好`,
            answer: [
              {
                text: ``,
                daima: ``
              }
            ]
          },
          {
            question: `19.说说对Css预编语言的理解？有哪些区别?`,
            description: ``,
            answer: [
              {
                text: `扩充了Css语言，增加了诸如变量、混合(mixin)、函数等功能，让Css更易维护、方便
本质上，预处理是Css的超集
包含一套自定义的语法及一个解析器，根据这些语法定义自己的样式规则，这些规则最终会通过解析
器，编译生成对应的Css文件
19.2.有哪些
udio
Css预编译语言在前端里面有三大优秀的预编处理器，分别是：
● sass
.less
stylus
Encode
19.2.1.sass
印客学院
2007 年诞生，最早也是最成熟的Css预处理器，拥有 Ruby 社区的支持和Compass这一最强大
的Css框架，目前受LESS影响，已经进化到了全面兼容Css的Scss
文件后缀名为·sass与scss，可以严格按照sass的缩进方式省去大括号和分号
19.2.2.less
2009年出现，受SASS的影响较大，但又使用Css的语法，让大部分开发者和设计师更容易上手，
在Ruby社区之外支持者远超过SASS
其缺点是比起SASS来，可编程功能不够，不过优点是简单和兼容Css，反过来也影响了SASS演
变到了Scss的时代
T
19.2.3. stylus
Stylus是一个Css的预处理框架，2010年产生，来自Node.js社区，主要用来给Node项目
进行Css预处理支持
所以Stylus是一种新型语言，可以创建健壮的、动态的、富有表现力的Css。比较年轻，其本质
上做的事情与SASS/LESS等类似
19.3. 区别
虽然各种预处理器功能强大，但使用最多的，还是以下特性：
● 变量(variables)
·作用域 (scope)
· 代码混合（mixins)
· 嵌套 (nested rules)
●代码模块化(Modules)
因此，下面就展开这些方面的区别
然后就是对应的具体的区别可以去网上面找找对应的资料，然后需要注意的是这个东西是可以结合这个css的解决方案来进行扩展的说一下的，比如使用
哪一些css方案，这些方案有哪一些区别，各自的优缺点有哪些`,
                daima: ``
              }
            ]
          },
          {
            question: `20.如果要做优化，CSS提高性能的方法有哪些?`,
            description: `20.1. 前言
每一个网页都离不开csS，但是很多人又认为，csS主要是用来完成页面布局的，像一些细节或者优
化，就不需要怎么考虑，实际上这种想法是不正确的
作为页面渲染和内容展现的重要环节，css影响着用户对整个网站的第一体验
因此，在整个产品研发过程中，css性能优化同样需要贯穿全程`,
            answer: [
              {
                text: `20.2.实现方式
实现方式有很多种，主要有如下：
·内联首屏关键CSS
异步加载CSS
●资源压缩
·合理使用选择器
·减少使用昂贵的属性
·不要使用@import
20.2.1. 内联首屏关键CSS
在打开一个页面，页面首要内容出现在屏幕的时间影响着用户的体验，而通过内联css关键代码能够
使浏览器在下载完html后就能立刻渲染
而如果外部引l用css代码，在解析html结构过程中遇到外部css文件，才会开始下载css代
码，再渲染
所以，CSS内联使用使渲染时间提前
注意：但是较大的css代码并不合适内联（初始拥塞窗口、没有缓存），而其余代码则采取外部引用
方式
20.2.2. 异步加载CSS
在CSS文件请求、下载、解析完成之前，CSS会阻塞渲染，浏览器将不会渲染任何已处理的内容
前面加载内联代码后，后面的外部引用cSs则没必要阻塞浏览器渲染。这时候就可以采取异步加载的
方案，主要有如下
●使用javascript将link标签插到head标签最后`,
                daima: `// 创建 link 标签
 const myCSS = document.createElement( "link" );
 myCSS.rel = "stylesheet";
 myCSS.href = "mystyles.css";
 // 插入到 header 的最后的位置
 document.head.insertBefore( myCSS, document.head.childNodes[ document.head.
 childNodes.length - 1 ].nextSibling );`
              },
              {
                text: `●设置link标签media属性为noexis，浏览器会认为当前样式表不适用当前类型，会在不阻塞页面渲染
的情况下再进行下载。加载完成后，将media的值设为screen或all，从而让浏览器开始解
析CSS`,
                daima: `<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.med
ia='all"">`
              },
              {
                text: `·通过rel属性将link元素标记为alternate可选样式表，也能实现浏览器异步加载。同样别忘了加载完成
之后，将rel设回stylesheet`,
                daima: `<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='styl
esheet "">`,
              },
              {
                text: ``,
                daima: `20.2.3.资源压缩
利用webpack、gulp/grunt、rollup等模块化工具,将css代码进行压缩，使文件变小,
大大降低了浏览器的加载时间
20.2.4.合理使用选择器
css匹配的规则是从右往左开始匹配，例如#markdown.contenth3匹配规则如下：
·先找到h3标签元素
●然后去除祖先不是.content的元素
·最后去除祖先不是#markdown的元素
如果嵌套的层级更多，页面中的元素更多，那么匹配所要花费的时间代价自然更高
所以我们在编写选择器的时候，可以遵循以下规则：
●不要嵌套使用过多复杂选择器，最好不要三层以上
·使用id选择器就没必要再进行嵌套
Studid
号
◆通配符和属性选择器效率最低，避免使用
20.2.5.减少使用昂贵的属性
在页面发生重绘的时候，昂贵属性如box-shadow/border-radius/filter/透明度/:nth-c
hild等，会降低浏览器的渲染性能
20.2.6. 不要使用@import
css样式文件有两种引l入方式，一种是link元素，另一种是@import
@import会影响浏览器的并行下载，使得页面在加载时增加额外的延迟，增添了额外的往返耗时
而且多个@import可能会导致下载顺序紊乱
比如一个css文件index.css包含了以下内容:@import url("reset.css")
那么浏览器就必须先把index.css下载、解析和执行后，才下载、解析和执行第二个文件reset.cs
s
20.2.7. 其他
●减少重排操作，以及减少不必要的重绘
●了解哪些属性可以继承而来，避免对这些属性重复编写
◆cssSprite，合成所有icon图片，用宽高加上backgroud-position的背景图方式显现出我们要的icon
图，减少了http请求
·把小的icon图片转成base64编码
●CSS3动画或者过渡尽量使用transform和opacity来实现动画，不要使用left和top属性
20.3.总结
css实现性能的方式可以从选择器嵌套、属性特性、减少http这三面考虑，同时还要注意css代
码的加载顺序`,
              }
            ]
          },
        ]
      },
      {
        tag: `ES6面试题`,
        data: [
          {
            question: `1. 说说var、let、const之间的区别`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 JavaScript 中，var、let 和 const 是用于声明变量的关键字，它们的主要区别体现在作用域、变量提升、重新赋值和块级作用域等方面。以下是详细对比：
一、作用域规则
1. var 的函数作用域
特点：变量作用域为整个函数，而非代码块。
示例：
javascript
function testVar() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10，变量提升至函数顶部
}

2. let 和 const 的块级作用域
特点：变量作用域仅限于最近的代码块（如 if、for、while 等）。
示例：
javascript
function testLet() {
  if (true) {
    let y = 20;
    const z = 30;
  }
  console.log(y); // ReferenceError: y 未定义
  console.log(z); // ReferenceError: z 未定义
}

二、变量提升
1. var 的变量提升
特点：变量声明会被提升至函数顶部，但赋值不会提升。
示例：
javascript
console.log(a); // undefined（变量提升，但未赋值）
var a = 1;

2. let 和 const 的暂时性死区（TDZ）
特点：变量声明不会被提升到作用域顶部，在声明前访问会触发 ReferenceError。
示例：
javascript
console.log(b); // ReferenceError: b 未定义
let b = 2;

console.log(c); // ReferenceError: c 未定义
const c = 3;

三、重复声明与重新赋值
1. var 允许重复声明
特点：同一作用域内可多次声明同一变量。
示例：
javascript
var d = 4;
var d = 5; // 合法，覆盖原有值

2. let 和 const 禁止重复声明
特点：同一作用域内重复声明会触发 SyntaxError。
示例：
javascript
let e = 6;
let e = 7; // SyntaxError: 标识符 'e' 已被声明

const f = 8;
const f = 9; // SyntaxError: 标识符 'f' 已被声明

3. const 禁止重新赋值
特点：const 声明的变量必须初始化，且不可重新赋值（但对象属性或数组元素可修改）。
示例：
javascript
const g = 10;
g = 11; // TypeError: 给常量赋值

const obj = { key: 1 };
obj.key = 2; // 合法，修改对象属性
obj = {}; // TypeError: 给常量赋值

四、循环中的行为差异
1. var 在循环中的问题
特点：循环变量会泄漏到外部作用域，且在异步操作中共享同一变量。
示例：
javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
}
console.log(i); // 3（泄漏到外部作用域）

2. let 和 const 在循环中的表现
特点：每次迭代都会创建独立的变量副本（闭包效果）。
示例：
javascript
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 输出 0, 1, 2
}
console.log(j); // ReferenceError: j 未定义

for (const k = 0; k < 3; k++) { // 报错：k++ 会修改常量
  // ...
}

五、面试答题要点
作用域：
var：函数作用域。
let/const：块级作用域（{} 内有效）。
变量提升：
var：声明提升，值为 undefined。
let/const：存在暂时性死区（TDZ），声明前不可访问。
重复声明与赋值：
var：可重复声明，可重新赋值。
let：不可重复声明，可重新赋值。
const：不可重复声明，不可重新赋值（对象属性除外）。
循环陷阱：
var：循环变量共享，异步操作易出错。
let：每次迭代创建独立副本，适合异步场景。
六、使用建议
优先使用 const：避免意外赋值，增强代码安全性。
需要重新赋值时使用 let：如循环计数器、动态变量。
避免使用 var：减少作用域泄漏和变量提升带来的问题。
总结
let 和 const 是 ES6 引入的块级作用域变量声明方式，解决了 var 的变量提升和作用域泄漏问题。const 通过禁止重新赋值增强了代码的健壮性，推荐在现代 JavaScript 中优先使用。`
              }
            ]
          },
          {
            question: `2.ES6中数组新增了哪些扩展?（这个貌似不重要）`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6（ES2015）中，数组新增了多项扩展，包括新的创建方式、遍历方法、操作方法和静态方法等。以下是核心扩展点及示例：
一、扩展运算符（Spread Operator）
作用：将数组展开为独立元素，用于解构赋值、函数调用、数组合并等。
javascript
// 解构赋值
const [a, ...rest] = [1, 2, 3, 4]; // a=1, rest=[2,3,4]

// 函数参数展开
const arr = [1, 2, 3];
Math.max(...arr); // 3

// 数组合并
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]

// 浅拷贝数组
const clone = [...arr];
二、新增静态方法
1. Array.from()
作用：将类数组对象（如 DOM 节点列表、arguments）或可迭代对象（如 Set、Map）转为真正的数组。
javascript
// 类数组对象转数组
const divs = document.querySelectorAll('div');
const arr = Array.from(divs);

// 字符串转数组
Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']

// 带映射函数
Array.from([1, 2, 3], x => x * 2); // [2, 4, 6]
2. Array.of()
作用：创建数组，解决 Array() 构造函数的怪异行为。
javascript
Array(3); // [empty × 3]（创建长度为3的空数组）
Array.of(3); // [3]（创建包含元素3的数组）
Array.of(1, 2, 3); // [1, 2, 3]
三、新增实例方法
1. find() 和 findIndex()
作用：查找符合条件的第一个元素（或索引）。
javascript
const arr = [10, 15, 20];
arr.find(x => x > 10); // 15
arr.findIndex(x => x > 10); // 1
2. fill()
作用：用固定值填充数组（可指定起始和结束位置）。
javascript
[1, 2, 3].fill(0); // [0, 0, 0]
[1, 2, 3].fill(0, 1, 2); // [1, 0, 3]（从索引1到2填充）
3. copyWithin()
作用：复制数组内部元素到指定位置（覆盖原有元素）。
javascript
[1, 2, 3, 4, 5].copyWithin(0, 3); // [4, 5, 3, 4, 5]
4. entries()、keys() 和 values()
作用：返回迭代器对象，用于遍历数组的键值对、键或值。
javascript
const arr = ['a', 'b', 'c'];
[...arr.entries()]; // [[0, 'a'], [1, 'b'], [2, 'c']]
[...arr.keys()]; // [0, 1, 2]
[...arr.values()]; // ['a', 'b', 'c']
5. includes()
作用：判断数组是否包含某个值（支持 NaN 比较）。
javascript
[1, 2, NaN].includes(NaN); // true
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(2, 2); // false（从索引2开始查找）
四、数组解构赋值
作用：简洁地从数组中提取值并赋值给变量。
javascript
const [x, y] = [1, 2]; // x=1, y=2

// 默认值
const [a = 10, b = 20] = [5]; // a=5, b=20

// 交换变量
let m = 1, n = 2;
[m, n] = [n, m]; // m=2, n=1
五、模板字符串与数组
作用：模板字符串可直接嵌入数组元素。
javascript
const arr = ['Alice', 'Bob'];
console.log('Names: ￥{ arr }'); // "Names: Alice,Bob"
六、面试答题要点
扩展运算符：
语法：...arr
应用：解构赋值、函数参数展开、数组合并。
静态方法：
Array.from()：类数组 / 可迭代对象转数组。
Array.of()：创建数组（避免 Array() 的怪异行为）。
实例方法：
find()/findIndex()：查找元素 / 索引。
fill()：填充数组。
copyWithin()：内部复制元素。
entries()/keys()/values()：返回迭代器。
includes()：判断包含关系（支持 NaN）。
解构赋值：
简洁提取数组值，支持默认值和交换变量。
七、总结
ES6 对数组的扩展增强了数组的创建、操作和遍历能力，使代码更简洁高效。扩展运算符和 Array.from() 是最常用的特性，而 find()、includes() 等方法提升了数组的实用性。掌握这些扩展是现代 JavaScript 开发的基础。`
              }
            ]
          },
          {
            question: `3.函数新增了哪些扩展?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6（ES2015）中，函数新增了多项重要扩展，包括箭头函数、参数默认值、剩余参数、扩展运算符、name 属性优化等。以下是核心扩展点及示例：
一、箭头函数（Arrow Functions）
作用：更简洁的函数语法，且不绑定自己的 this、arguments、super 或 new.target。
javascript
// 基本语法
const sum = (a, b) => a + b;

// 无参数或多个参数需括号
const greet = () => 'Hello';
const multiply = (a, b) => a * b;

// 函数体多行需用大括号和return
const getPerson = () => {
  return { name: 'Alice', age: 30 };
};

// 不绑定this（继承自外层作用域）
const obj = {
  name: 'Bob',
  sayHi: () => {
    console.log(this.name); // 非箭头函数：'Bob'；箭头函数：undefined（继承全局this）
  }
};
二、参数默认值
作用：直接在参数定义中设置默认值，替代传统的条件判断。
javascript
// 基本用法
function greet(name = 'Guest') {
  return 'Hello, ￥{ name }';
}
greet(); // "Hello, Guest"
greet('Alice'); // "Hello, Alice"

// 默认值可引用其他参数
function calculate(a, b = a * 2) {
  return a + b;
}
calculate(3); // 9（3 + 3*2）
三、剩余参数（Rest Parameters）
作用：将多个参数收集为一个数组，替代 arguments 对象。
javascript
// 基本用法
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
sum(1, 2, 3); // 6

// 与普通参数结合
function greet(host, ...guests) {
  console.log('Host: ￥{ host }, Guests: ￥{ guests.join(', ') }');
}
greet('Alice', 'Bob', 'Charlie'); // "Host: Alice, Guests: Bob, Charlie"
四、扩展运算符（Spread Operator）在函数调用中的应用
作用：将数组展开为多个参数，用于函数调用。
javascript
// 替代apply()
const numbers = [1, 2, 3];
Math.max(...numbers); // 3（等价于 Math.max.apply(null, numbers)）

// 合并参数
function add(a, b, c) {
  return a + b + c;
}
add(...[1, 2], 3); // 6
五、name 属性优化
作用：函数对象的 name 属性返回更准确的函数名（包括匿名函数）。
javascript
function namedFunc() {}
namedFunc.name; // "namedFunc"

const arrow = () => {};
arrow.name; // "arrow"

const obj = {
  sayHi() {}
};
obj.sayHi.name; // "sayHi"

// 构造函数的name属性
class Person {}
new Person().constructor.name; // "Person"
六、尾调用优化（Tail Call Optimization, TCO）
作用：优化尾递归函数，避免栈溢出（需引擎支持，如 Safari）。
javascript
// 普通递归（可能栈溢出）
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // 非尾递归：最后一步是乘法运算
}

// 尾递归（优化后栈深度为O(1)）
function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTail(n - 1, n * accumulator); // 尾递归：最后一步是直接返回递归调用
}
七、面试答题要点
箭头函数：
语法更简洁，无自己的 this，适合纯函数和回调。
不能使用 arguments、yield，不适合构造函数。
参数默认值：
直接在参数列表中设置默认值，支持表达式和引用其他参数。
剩余参数：
语法：...args，将多个参数收集为数组。
替代 arguments 对象，且仅包含剩余参数。
扩展运算符：
语法：...arr，将数组展开为参数序列。
用于函数调用、数组合并等。
name 属性：
更准确地反映函数名称（包括匿名函数和方法）。
尾调用优化：
优化尾递归，避免栈溢出（需引擎支持）。
八、总结
ES6 对函数的扩展主要集中在语法糖（箭头函数、参数默认值）和参数处理（剩余参数、扩展运算符）上，同时优化了函数的元数据（name 属性）和性能（尾调用优化）。这些特性使 JavaScript 函数更灵活、简洁，且减少了常见的陷阱（如 this 指向问题）。`
              }
            ]
          },
          {
            question: `4.对象新增了哪些扩展？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6（ES2015）中，对象新增了多项重要扩展，包括字面量语法增强、静态方法、解构赋值、扩展运算符等。以下是核心扩展点及示例：
一、对象字面量增强
1. 属性简写
作用：当对象属性名与变量名相同时，可省略属性值。
javascript
const name = 'Alice';
const age = 30;
const person = { name, age }; // 等价于 { name: name, age: age }
2. 方法简写
作用：省略 function 关键字，直接定义方法。
javascript
const obj = {
  greet() {
    return 'Hello';
  },
  // 等价于 ES5: greet: function() { return 'Hello'; }
};
3. 计算属性名
作用：使用表达式作为属性名，需用方括号包裹。
javascript
const key = 'name';
const obj = {
  [key]: 'Alice',
  ['￥{ key }Length']: 'Alice'.length
};
// 结果：{ name: 'Alice', nameLength: 5 }
二、新增静态方法
1. Object.assign()
作用：浅拷贝对象的所有可枚举属性到目标对象。
javascript
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source); // { a: 1, b: 2 }

// 合并多个源对象
const merged = Object.assign({}, source1, source2);

// 注意：浅拷贝，嵌套对象仅复制引用
const obj = { nested: { key: 1 } };
const clone = Object.assign({}, obj);
clone.nested.key = 2; // 修改克隆对象会影响原对象
2. Object.is()
作用：比较两个值是否严格相等（修复 === 的一些边缘情况）。
javascript
Object.is(NaN, NaN); // true（=== 返回 false）
Object.is(0, -0); // false（=== 返回 true）
Object.is(5, 5); // true
3. Object.setPrototypeOf()
作用：动态设置对象的原型（替代 __proto__）。
javascript
const parent = { method() {} };
const child = {};
Object.setPrototypeOf(child, parent); // child 继承自 parent
三、解构赋值
作用：从对象中提取值并赋值给变量。
javascript
const person = { name: 'Bob', age: 30 };
const { name, age } = person; // name = 'Bob', age = 30

// 重命名变量
const { name: fullName, age: years } = person; // fullName = 'Bob', years = 30

// 默认值
const { city = 'Unknown' } = person; // city = 'Unknown'

// 嵌套解构
const obj = { nested: { key: 'value' } };
const { nested: { key } } = obj; // key = 'value'
四、扩展运算符（Spread Operator）
作用：展开对象的所有可枚举属性，用于对象合并或浅拷贝。
javascript
// 浅拷贝对象
const obj = { a: 1, b: 2 };
const clone = { ...obj }; // { a: 1, b: 2 }

// 合并对象（后面的属性覆盖前面的）
const merged = { ...obj, c: 3, b: 4 }; // { a: 1, b: 4, c: 3 }

// 与解构赋值结合（收集剩余属性）
const { a, ...rest } = obj; // a = 1, rest = { b: 2 }
五、原型链增强
1. Object.getPrototypeOf()
作用：获取对象的原型（替代 __proto__）。
javascript
const obj = {};
Object.getPrototypeOf(obj) === Object.prototype; // true
2. Object.keys()、values() 和 entries()
作用：返回对象的键、值或键值对数组。
javascript
const obj = { a: 1, b: 2 };
Object.keys(obj); // ['a', 'b']
Object.values(obj); // [1, 2]
Object.entries(obj); // [['a', 1], ['b', 2]]
六、面试答题要点
字面量语法增强：
属性简写：{ name, age }
方法简写：{ greet() {} }
计算属性名：{ [key]: value }
静态方法：
Object.assign()：浅拷贝 / 合并对象。
Object.is()：精确比较值。
Object.setPrototypeOf()：设置原型。
解构赋值：
提取对象属性，支持重命名和默认值。
扩展运算符：
展开对象属性，用于合并或浅拷贝。
原型链操作：
Object.getPrototypeOf()：获取原型。
Object.keys()/values()/entries()：遍历对象。
七、总结
ES6 对对象的扩展主要集中在语法糖（简写、计算属性）、实用工具（assign、is）、解构赋值和扩展运算符上。这些特性使对象操作更简洁、灵活，减少了样板代码，并提供了更强大的对象处理能力。`
              }
            ]
          },
          {
            question: `5.你是怎么理解ES6中Promise的?使用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Promise 是一种异步编程的解决方案，用于处理异步操作的结果（成功或失败），避免回调地狱，使代码更易读和维护。以下是核心概念、应用场景及面试答题要点：
一、Promise 的核心概念
1. 三种状态
pending：初始状态，既不是成功也不是失败。
fulfilled：操作成功完成。
rejected：操作失败。
状态一旦改变，就永久保持该状态，不会再变。
2. 基本语法
javascript
const promise = new Promise((resolve, reject) => {
  // 异步操作（如网络请求、定时器）
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve('操作成功'); // 状态变为 fulfilled
    } else {
      reject(new Error('操作失败')); // 状态变为 rejected
    }
  }, 1000);
});

// 处理结果
promise
  .then((value) => console.log(value)) // 成功回调
  .catch((error) => console.error(error)) // 失败回调
  .finally(() => console.log('无论成功失败都执行'));
3. 链式调用
Promise 的 then 和 catch 方法返回新的 Promise，支持链式调用：
javascript
fetchData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .catch((error) => handleError(error));
二、Promise 的静态方法
1. Promise.all()
作用：并行处理多个 Promise，全部成功时返回结果数组，任一失败则立即终止。
javascript
const promise1 = fetch('/api/data1');
const promise2 = fetch('/api/data2');

Promise.all([promise1, promise2])
  .then(([data1, data2]) => console.log(data1, data2))
  .catch((error) => console.error(error));
2. Promise.race()
作用：多个 Promise 竞争，返回第一个完成（成功或失败）的 Promise 结果。
javascript
const fetchPromise = fetch('/api/data');
const timeoutPromise = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('请求超时')), 5000);
});

Promise.race([fetchPromise, timeoutPromise])
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
3. Promise.allSettled()
作用：返回所有 Promise 的结果（无论成功或失败），适用于需要处理所有结果的场景。
javascript
Promise.allSettled([promise1, promise2]).then((results) => {
  results.forEach((result) => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value);
    } else {
      console.log('失败:', result.reason);
    }
  });
});
4. Promise.resolve() 和 Promise.reject()
作用：快速创建已解决或已拒绝的 Promise。
javascript
const resolvedPromise = Promise.resolve('直接成功');
const rejectedPromise = Promise.reject(new Error('直接失败'));
三、应用场景
1. 异步操作串行化
场景：需要按顺序执行多个异步操作（如依次发送多个 API 请求）。
javascript
loginUser()
  .then((token) => fetchUserData(token))
  .then((userData) => saveUserData(userData))
  .catch((error) => console.error(error));
2. 并行异步操作
场景：多个独立异步操作可同时执行，全部完成后处理结果（如批量上传文件）。
javascript
const uploadPromises = files.map((file) => uploadFile(file));
Promise.all(uploadPromises)
  .then(() => console.log('全部上传成功'))
  .catch((error) => console.error('有文件上传失败:', error));
3. 处理异步错误
场景：统一捕获和处理异步操作中的错误，避免层层嵌套的 try-catch。
javascript
fetch('/api/data')
  .then((response) => response.json())
  .then((data) => processData(data))
  .catch((error) => {
    console.error('请求出错:', error);
    showErrorToast('数据加载失败');
  });
4. 异步超时控制
场景：为异步操作设置超时限制，避免长时间等待。
javascript
const fetchWithTimeout = (url, timeout) => {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) => setTimeout(() => reject(new Error('超时')), timeout))
  ]);
};
5. 回调地狱的替代方案
场景：避免多层嵌套的回调函数，使代码更扁平化。
javascript
// 回调地狱
getData((err, data1) => {
  if (err) throw err;
  processData(data1, (err, data2) => {
    if (err) throw err;
    saveData(data2, (err) => {
      if (err) throw err;
      console.log('完成');
    });
  });
});

// 使用Promise改进
getData()
  .then(processData)
  .then(saveData)
  .then(() => console.log('完成'))
  .catch((err) => console.error(err));
四、面试答题要点
核心概念：
状态机：pending → fulfilled/rejected（不可逆）。
解决回调地狱，支持链式调用。
关键 API：
then()：处理成功结果，返回新 Promise。
catch()：处理失败结果，返回新 Promise。
finally()：无论状态如何都会执行。
静态方法：all、race、allSettled、resolve、reject。
应用场景：
异步操作串行化 / 并行化。
错误统一处理。
超时控制。
替代回调地狱。
优缺点：
优点：代码更清晰，支持并发控制，统一错误处理。
缺点：无法中途取消，调试较复杂，多层嵌套仍存在可读性问题（但优于回调）。
五、总结
Promise 是 ES6 中解决异步编程的核心工具，通过状态管理和链式调用，有效避免了回调地狱，使异步代码更易维护。其静态方法提供了强大的并发控制能力，广泛应用于网络请求、文件操作等异步场景。后续 ES8 的 async/await 语法基于 Promise 进一步优化了异步代码的写法，使其更接近同步代码的可读性。`
              }
            ]
          },
          {
            question: `6.你是怎么理解ES6中Module的?使用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Module（模块）是一种将代码分割为独立功能单元的机制，用于解决代码组织、依赖管理和命名冲突问题。以下是核心概念、应用场景及面试答题要点：
一、Module 的核心概念
1. 导出（Export）
作用：暴露模块内部的变量、函数或类，使其可被其他模块导入。
javascript
// math.js
export const PI = 3.14;
export function add(a, b) {
  return a + b;
}

// 或集中导出
const PI = 3.14;
function add(a, b) {
  return a + b;
}
export { PI, add };

// 默认导出（每个模块只能有一个）
export default function multiply(a, b) {
  return a * b;
}
2. 导入（Import）
作用：引入其他模块导出的内容。
javascript
// main.js
// 按需导入
import { PI, add } from './math.js';
console.log(PI); // 3.14
console.log(add(1, 2)); // 3

// 导入默认导出
import multiply from './math.js';
console.log(multiply(2, 3)); // 6

// 导入全部内容为命名空间
import * as math from './math.js';
console.log(math.PI); // 3.14
console.log(math.add(1, 2)); // 3
3. 模块特性
静态结构：导入导出语句必须位于文件顶层，编译时确定依赖关系。
单例模式：模块只会被执行一次，多次导入共享同一实例。
严格模式：模块内默认启用严格模式（'use strict'）。
二、模块加载方式
1. 浏览器环境
html
预览
《script2 type="module" src="main.js">《/script2 >
  特点：
  自动延迟执行（相当于 defer 属性）。
  支持相对路径和绝对路径，需服务器环境（避免跨域问题）。
  2. Node.js 环境
  ES 模块（.mjs）：需在 package.json 中添加 "type": "module"。
  javascript
  // main.mjs
  import { PI } from './math.mjs';

  CommonJS（.cjs）：传统 Node 模块格式，使用 require 和 module.exports。
  三、应用场景
  1. 代码组织与复用
  场景：将大型项目拆分为多个小模块，提高代码可维护性。
  javascript
  // utils/format.js
  export function formatDate(date) {
  return date.toISOString().split('T')[0];
  }

  // utils/validate.js
  export function isValidEmail(email) {
  return /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email);
  }

  // main.js
  import { formatDate } from './utils/format.js';
  import { isValidEmail } from './utils/validate.js';
  2. 依赖管理
  场景：明确模块间的依赖关系，避免全局变量污染。
  javascript
  // 模块A
  export const data = [];

  // 模块B
  import { data } from './moduleA.js';
  data.push('new item'); // 直接修改模块A的数据
  3. 按需加载
  场景：动态加载非必需模块（如路由懒加载）。
  javascript
  // 动态导入（返回Promise）
  button.addEventListener('click', async () => {
  const { heavyModule } = await import('./heavy-module.js');
  heavyModule.init();
  });
  4. 第三方库集成
  场景：引入外部库并选择性导入所需功能。
  javascript
  // 导入完整库
  import _ from 'lodash';
  console.log(_.cloneDeep({ a: 1 }));

  // 按需导入
  import { cloneDeep } from 'lodash-es'; // lodash的ES模块版本
  console.log(cloneDeep({ a: 1 }));
  5. 命名空间隔离
  场景：避免不同模块间的命名冲突。
  javascript
  // moduleA.js
  export const name = 'Alice';

  // moduleB.js
  export const name = 'Bob';

  // main.js
  import { name as nameA } from './moduleA.js';
  import { name as nameB } from './moduleB.js';
  console.log(nameA, nameB); // "Alice Bob"
  四、面试答题要点
  核心概念：
  导出 / 导入：export 和 import 语句。
  静态结构：编译时确定依赖关系。
  单例模式：模块只执行一次，多次导入共享实例。
  与 CommonJS 的对比：
  ES Module：静态导入，编译时分析，支持浏览器和 Node。
  CommonJS：动态导入（require），运行时分析，主要用于 Node。
  应用场景：
  大型项目的代码组织。
  依赖管理与命名空间隔离。
  按需加载与性能优化。
  第三方库的模块化使用。
  浏览器支持：
  通过
  <script1 type="module"> 使用。
    需服务器环境（避免跨域）。
    支持模块预加载（rel="modulepreload"）。
    五、总结
    ES6 Module 是 JavaScript 官方的模块化解决方案，通过 export 和 import 实现代码的封装与复用，解决了传统全局变量和命名空间的问题。其静态结构特性使代码更易于优化（如 Tree
    Shaking），并为现代前端构建工具（如 Webpack、Rollup）提供了基础。在实际开发中，模块机制广泛应用于项目架构设计、依赖管理和性能优化等场景。`
              }
            ]
          },
          {
            question: `7.你是怎么理解ES6中Generator的?使用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Generator（生成器）是一种特殊的函数，可以暂停执行并在稍后恢复，为异步编程和迭代器提供了强大支持。以下是核心概念、应用场景及面试答题要点：
    一、Generator 的核心概念
    1. 基本语法
    作用：通过 function* 和 yield 创建可暂停的函数。
    javascript
    function* generatorFunction() {
    yield 1;
    yield 2;
    return 3;
    }

    const gen = generatorFunction();
    console.log(gen.next()); // { value: 1, done: false }
    console.log(gen.next()); // { value: 2, done: false }
    console.log(gen.next()); // { value: 3, done: true }
    2. 执行机制
    暂停 / 恢复：每次调用 next() 时，函数执行到下一个 yield 并暂停。
    双向通信：
    next(value)：向生成器内部传递值（作为上一个 yield 的返回值）。
    yield expression：向外返回值。
    3. 关键方法
    next()：返回 { value, done } 对象，继续执行到下一个 yield。
    return(value)：终止生成器并返回指定值。
    throw(error)：在生成器内部抛出错误。
    二、Generator 的应用场景
    1. 自定义迭代器
    场景：创建可自定义遍历逻辑的对象。
    javascript
    function* range(start, end) {
    for (let i = start; i <= end; i++) { yield i; } } for (const num of range(1, 3)) { console.log(num); // 1, 2, 3 } 2.
      异步编程（Generator + Promise） 场景：简化异步代码，替代回调地狱（ES8 中被 async/await 取代）。 javascript function fetchData(url) { return new
      Promise((resolve)=> {
      setTimeout(() => resolve('Data from ￥{ url }'), 1000);
      });
      }

      function* asyncFlow() {
      const data1 = yield fetchData('/api/data1');
      const data2 = yield fetchData('/api/data2');
      return [data1, data2];
      }

      // 执行器（自动运行Generator）
      function run(gen) {
      const iterator = gen();
      function iterate(iteration) {
      if (iteration.done) return iteration.value;
      iteration.value.then((value) => {
      iterate(iterator.next(value));
      });
      }
      iterate(iterator.next());
      }

      run(asyncFlow);
      3. 惰性计算（Lazy Evaluation）
      场景：按需生成大量数据，避免一次性计算。
      javascript
      function* infiniteSequence() {
      let i = 0;
      while (true) {
      yield i++;
      }
      }

      const seq = infiniteSequence();
      console.log(seq.next().value); // 0
      console.log(seq.next().value); // 1
      // 可无限生成，不占用额外内存
      4. 控制流管理
      场景：实现协作式多任务（如任务调度）。
      javascript
      function* task1() {
      console.log('Task 1: Step 1');
      yield;
      console.log('Task 1: Step 2');
      }

      function* task2() {
      console.log('Task 2: Step 1');
      yield;
      console.log('Task 2: Step 2');
      }

      const tasks = [task1(), task2()];
      function scheduler() {
      let taskIndex = 0;
      while (tasks.some((task) => !task.next().done)) {
      tasks[taskIndex].next();
      taskIndex = (taskIndex + 1) % tasks.length;
      }
      }
      scheduler();
      5. 生成唯一 ID
      场景：创建自增或随机 ID 生成器。
      javascript
      function* idGenerator() {
      let id = 1;
      while (true) {
      yield id++;
      }
      }

      const gen = idGenerator();
      console.log(gen.next().value); // 1
      console.log(gen.next().value); // 2
      三、面试答题要点
      核心概念：
      暂停执行：通过 yield 暂停，next() 恢复。
      双向通信：next(value) 传值，yield 返回值。
      迭代器协议：生成器对象实现了 Symbol.iterator。
      与普通函数的区别：
      普通函数：一次执行完，返回单个值。
      Generator：可暂停 / 恢复，返回多个值（通过迭代器）。
      应用场景：
      自定义迭代器（如范围、无限序列）。
      异步编程（Generator + Promise，后被 async/await 替代）。
      惰性计算与性能优化。
      控制流管理（如任务调度）。
      优缺点：
      优点：代码更简洁，避免回调地狱；支持惰性计算。
      缺点：需手动管理迭代器（如使用执行器）；调试较复杂。
      四、总结
      Generator 是 ES6 中用于创建可暂停函数的特殊语法，通过 yield 和 next() 实现了强大的控制流能力。其核心应用场景包括自定义迭代器、异步编程（早期 Promise 的协作方案）和惰性计算。尽管现代
      JavaScript 中 async/await 已基本取代了 Generator 在异步编程中的地位，但生成器在需要精细控制执行流程的场景中仍有价值。`
              }
            ]
          },
          {
            question: `8.你是怎么理解ES6中Decorator的？使用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Decorator（装饰器）是一种元编程语法，用于在不修改原代码的前提下，动态地为类、方法或属性添加额外功能。它通过 @
      符号来使用，本质是一个函数，可对目标进行拦截和增强。以下是核心概念、应用场景及面试答题要点：
      一、Decorator 的核心概念
      1. 基本语法
      javascript
      // 类装饰器
      @logClass
      class MyClass {
      // ...
      }

      // 方法装饰器
      class MyClass {
      @logMethod
      myMethod() {
      // ...
      }
      }

      // 属性装饰器
      class MyClass {
      @readonly
      myProperty = 'value';
      }
      2. 装饰器函数
      装饰器本质是一个函数，根据目标类型接收不同参数：
      javascript
      // 类装饰器（接收构造函数）
      function logClass(constructor) {
      return class extends constructor {
      constructor(...args) {
      super(...args);
      console.log('Created instance of ￥{ constructor.name }');
      }
      };
      }

      // 方法装饰器（接收目标对象、属性名和属性描述符）
      function logMethod(target, name, descriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args) {
      console.log('Calling ￥{ name } with args: ￥{ args }');
      const result = originalMethod.apply(this, args);
      console.log('Method ￥{ name } returned: ￥{ result }');
      return result;
      };
      return descriptor;
      }
      二、应用场景
      1. 日志记录
      场景：自动记录方法调用信息（如参数、返回值）。
      javascript
      class UserService {
      @logMethod
      getUser(id) {
      return { id, name: 'John' };
      }
      }

      // 输出：
      // Calling getUser with args: 123
      // Method getUser returned: { id: 123, name: 'John' }
      2. 权限控制
      场景：拦截方法调用，验证用户权限。
      javascript
      function authRequired(target, name, descriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args) {
      if (!this.isAuthenticated) {
      throw new Error('Authentication required');
      }
      return originalMethod.apply(this, args);
      };
      return descriptor;
      }

      class Dashboard {
      isAuthenticated = false;

      @authRequired
      editSettings() {
      // ...
      }
      }
      3. 性能监控
      场景：测量方法执行时间。
      javascript
      function performanceLog(target, name, descriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = async function(...args) {
      const start = Date.now();
      const result = await originalMethod.apply(this, args);
      const end = Date.now();
      console.log('￥{ name } executed in ￥{ end - start}ms');
      return result;
      };
      return descriptor;
      }

      class DataFetcher {
      @performanceLog
      async fetchData() {
      // 模拟网络请求
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return { data: 'result' };
      }
      }
      4. 自动绑定
      场景：确保类方法的 this 指向实例。
      javascript
      function autoBind(target, name, descriptor) {
      const originalMethod = descriptor.value;
      let boundMethod;
      return {
      configurable: true,
      get() {
      if (!boundMethod) {
      boundMethod = originalMethod.bind(this);
      }
      return boundMethod;
      }
      };
      }

      class Button {
      @autoBind
      onClick() {
      console.log('Clicked', this);
      }
      }
      5. 类型检查
      场景：在运行时验证参数类型。
      javascript
      function validateParams(type) {
      return (target, name, descriptor) => {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args) {
      args.forEach((arg) => {
      if (typeof arg !== type) {
      throw new TypeError('Expected ￥{ type }, got ￥{ typeof arg }');
      }
      });
      return originalMethod.apply(this, args);
      };
      return descriptor;
      };
      }

      class Calculator {
      @validateParams('number')
      add(a, b) {
      return a + b;
      }
      }
      三、面试答题要点
      核心概念：
      元编程：在不修改原代码的前提下增强功能。
      执行顺序：多个装饰器从下到上（或从内到外）依次执行。
      参数类型：根据目标（类、方法、属性）接收不同参数。
      应用场景：
      日志记录与监控。
      权限控制与访问限制。
      性能优化（如自动绑定、缓存）。
      类型检查与数据验证。
      AOP（面向切面编程）。
      现状与局限：
      TC39 阶段 2：尚未成为正式标准，需通过 Babel 或 TypeScript 支持。
      仅用于类和类成员：无法装饰函数（因函数提升问题）。
      与 Mixin 的对比：
      Decorator：聚焦于行为增强，可组合使用。
      Mixin：通过继承复用代码，适合功能复用。
      四、总结
      Decorator 是一种强大的元编程工具，通过函数式的方式为类和类成员添加额外行为，实现了关注点分离和代码复用。常见应用包括日志、权限控制、性能监控等。尽管目前处于提案阶段，但已在主流框架（如
      Angular、NestJS）中广泛使用。理解装饰器的工作原理和应用场景，有助于编写更简洁、更具扩展性的代码。`
              }
            ]
          },
          {
            question: `9.你是怎么理解ES6新增Set、Map两种数据结构的?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Set 和 Map 是两种新增的数据结构，分别用于存储唯一值的集合和键值对映射，弥补了传统对象和数组的不足。以下是核心概念、应用场景及面试答题要点：
      一、Set 的核心特性
      1. 基本概念
      唯一值集合：不允许重复值，可用于去重。
      顺序存储：按插入顺序存储元素，支持迭代。
      2. 关键 API
      javascript
      const set = new Set([1, 2, 2, 3]); // Set(3) {1, 2, 3}

      // 添加/删除元素
      set.add(4); // Set(4) {1, 2, 3, 4}
      set.delete(2); // true
      set.has(3); // true
      set.size; // 3

      // 迭代
      set.forEach((value) => console.log(value));
      for (const value of set) { /* ... */ }

      // 转换为数组
      const arr = [...set]; // [1, 3, 4]
      3. 应用场景
      数组去重：
      javascript
      const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]

      交集 / 并集 / 差集：
      javascript
      const setA = new Set([1, 2, 3]);
      const setB = new Set([3, 4, 5]);

      // 交集
      const intersection = new Set([...setA].filter((x) => setB.has(x))); // Set(1) {3}

      // 并集
      const union = new Set([...setA, ...setB]); // Set(5) {1, 2, 3, 4, 5}

      // 差集
      const difference = new Set([...setA].filter((x) => !setB.has(x))); // Set(2) {1, 2}

      二、Map 的核心特性
      1. 基本概念
      键值对映射：键可以是任意类型（对象、函数等），不同于普通对象只能使用字符串或 Symbol。
      顺序存储：按插入顺序存储键值对，支持迭代。
      2. 关键 API
      javascript
      const map = new Map();
      const keyObj = { id: 1 };

      // 设置/获取值
      map.set(keyObj, 'value');
      map.get(keyObj); // 'value'
      map.has(keyObj); // true
      map.delete(keyObj); // true
      map.size; // 0

      // 迭代
      map.forEach((value, key) => console.log(key, value));
      for (const [key, value] of map) { /* ... */ }

      // 转换为数组
      const arr = Array.from(map); // [[keyObj, 'value']]
      3. 应用场景
      缓存数据：
      javascript
      const cache = new Map();

      function fetchData(id) {
      if (cache.has(id)) {
      return cache.get(id);
      }
      const data = apiCall(id);
      cache.set(id, data);
      return data;
      }

      复杂键的映射：
      javascript
      const eventHandlers = new Map();
      const element = document.getElementById('button');

      eventHandlers.set(element, () => {
      console.log('Button clicked');
      });

      三、与传统数据结构的对比
      特性 Set Map 普通对象 数组
      键类型 值本身（任意类型） 任意类型 字符串或 Symbol 数值索引
      唯一性 值唯一 键唯一 键重复会覆盖 允许重复值
      顺序性 插入顺序 插入顺序 不保证顺序 按索引顺序
      迭代支持 支持 支持 需手动处理 支持
      大小获取 size 属性 size 属性 需手动计算 length 属性
      查找效率 O (1)（基于哈希） O (1)（基于哈希） O (n)（需遍历） O (n)（需遍历）
      四、面试答题要点
      Set 的核心：
      存储唯一值，支持高效去重。
      适合数学集合运算（交、并、差）。
      Map 的核心：
      键可以是任意类型，解决普通对象键只能是字符串的限制。
      有序存储，适合需要保留插入顺序的场景。
      应用场景选择：
      Set：去重、存在性检查、集合运算。
      Map：缓存、复杂键映射、需要顺序的键值对。
      性能优势：
      Set 和 Map 的查找、插入、删除操作时间复杂度均为 O (1)，优于数组的 O (n)。
      五、总结
      ES6 新增的 Set 和 Map 数据结构通过唯一性和任意键类型，解决了传统对象和数组的局限性，提供了更高效、更灵活的数据存储方式。Set 适合处理唯一值集合，Map 则擅长复杂键的映射和有序存储。在实际开发中，合理使用
      Set 和 Map 可以提升代码的可读性和性能。`
              }
            ]
          },
          {
            question: `9.1 WeakSet和WeakMap`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，WeakSet 和 WeakMap 是两种特殊的数据结构，它们与 Set/Map 类似，但在内存管理和使用场景上有显著差异。以下是核心概念、应用场景及面试答题要点：
      一、WeakSet 的核心特性
      1. 基本概念
      弱引用集合：只能存储对象，且对对象的引用是弱引用（不阻止对象被垃圾回收）。
      不可枚举：不支持迭代器（如keys()、values()），没有size属性。
      2. 关键 API
      javascript
      const weakSet = new WeakSet();
      const obj = {};

      // 添加/删除元素
      weakSet.add(obj);
      weakSet.has(obj); // true
      weakSet.delete(obj); // true

      // 无法遍历或获取大小
      weakSet.size; // undefined
      weakSet.forEach(() => {}); // 报错
      3. 内存管理特点
      当 WeakSet 存储的对象被其他地方释放时，WeakSet 中的引用不会阻止垃圾回收：
      javascript
      let obj = { key: 'value' };
      const weakSet = new WeakSet([obj]);

      obj = null; // 对象的唯一强引用被移除，WeakSet中的弱引用不阻止GC
      // 此时obj可能被垃圾回收，WeakSet自动清空该引用

      二、WeakMap 的核心特性
      1. 基本概念
      弱引用映射：只能使用对象作为键，且对键的引用是弱引用。
      不可枚举：不支持迭代，没有size属性，无法清空所有元素。
      2. 关键 API
      javascript
      const weakMap = new WeakMap();
      const key = {};

      // 设置/获取值
      weakMap.set(key, 'value');
      weakMap.get(key); // 'value'
      weakMap.has(key); // true
      weakMap.delete(key); // true

      // 无法遍历或获取大小
      weakMap.size; // undefined
      weakMap.forEach(() => {}); // 报错
      3. 内存管理特点
      当 WeakMap 的键对象被其他地方释放时，对应的键值对会自动被垃圾回收：
      javascript
      let key = { id: 1 };
      const weakMap = new WeakMap();
      weakMap.set(key, 'data');

      key = null; // 键对象的唯一强引用被移除
      // 此时WeakMap中的键值对会被自动删除（下次GC时）

      三、应用场景
      1. 关联元数据
      场景：为对象附加数据，且不影响对象生命周期。
      javascript
      // 使用WeakMap存储DOM元素的元数据
      const elementData = new WeakMap();
      const button = document.getElementById('button');

      elementData.set(button, {
      clickCount: 0,
      handler: () => { /* ... */ }
      });

      // 当button元素被DOM移除并垃圾回收时，WeakMap中的数据自动清除
      2. 私有属性
      场景：在类中实现真正的私有属性（外部无法访问）。
      javascript
      const privateData = new WeakMap();

      class MyClass {
      constructor() {
      privateData.set(this, {
      secret: 'private value'
      });
      }

      getSecret() {
      return privateData.get(this).secret;
      }
      }

      const instance = new MyClass();
      console.log(instance.getSecret()); // 'private value'
      console.log(privateData.get(instance)); // 外部无法直接访问
      3. 缓存计算结果
      场景：缓存对象的计算结果，避免重复计算。
      javascript
      const cache = new WeakMap();

      function compute(obj) {
      if (cache.has(obj)) {
      return cache.get(obj);
      }
      const result = expensiveOperation(obj);
      cache.set(obj, result);
      return result;
      }
      4. 事件监听器存储
      场景：存储 DOM 元素的事件监听器，避免内存泄漏。
      javascript
      const listeners = new WeakMap();

      function addListener(element, event, handler) {
      if (!listeners.has(element)) {
      listeners.set(element, new Set());
      }
      listeners.get(element).add(handler);
      element.addEventListener(event, handler);
      }

      function removeAllListeners(element) {
      if (listeners.has(element)) {
      listeners.get(element).forEach((handler) => {
      element.removeEventListener(handler);
      });
      listeners.delete(element);
      }
      }
      四、与 Set/Map 的对比
      特性 WeakSet WeakMap Set Map
      键类型 只能是对象 键只能是对象 任意类型 任意类型
      引用类型 弱引用 键为弱引用 强引用 强引用
      可枚举性 否 否 是 是
      垃圾回收 对象被回收时自动移除 键对象被回收时自动移除 需手动删除 需手动删除
      应用场景 存储对象集合（去重） 存储对象关联数据 存储唯一值集合 存储键值对映射
      五、面试答题要点
      核心区别：
      弱引用：WeakSet/WeakMap 对对象的引用不会阻止垃圾回收。
      不可枚举：无法遍历或获取大小，只能通过已知键访问。
      设计目的：
      避免内存泄漏：当对象不再被其他地方引用时，WeakSet/WeakMap 中的关联数据自动清除。
      存储元数据：为对象附加临时数据，不影响对象生命周期。
      性能考量：
      由于垃圾回收机制不确定，WeakSet/WeakMap 的清理时机不可预测，不适合需要精确控制内存的场景。
      典型应用：
      私有属性 / 方法（WeakMap）。
      DOM 元素关联数据（WeakMap）。
      临时缓存（WeakMap）。
      对象去重（WeakSet）。
      六、总结
      WeakSet 和 WeakMap 通过弱引用机制，为 JavaScript 提供了更安全的内存管理方式。它们适用于需要临时存储对象关联数据，且不希望影响对象生命周期的场景。与 Set/Map
      相比，WeakSet/WeakMap 的不可枚举性和弱引用特性使其在内存优化和元数据管理上具有独特优势，但也限制了它们的使用范围。在实际开发中，应根据数据生命周期和访问需求选择合适的数据结构。`
              }
            ]
          },
          {
            question: `10.你是怎么理解ES6中Proxy的?使用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 ES6 中，Proxy 是一种元编程工具，用于拦截并自定义对象的基本操作（如属性访问、赋值、枚举、函数调用等）。它为对象提供了一层
      “代理”，允许开发者在不修改原对象的前提下，对其行为进行增强或修改。以下是核心概念、应用场景及面试答题要点：
      一、Proxy 的核心概念
      1. 基本语法
      javascript
      const target = { name: 'Alice', age: 30 };

      const handler = {
      // 拦截属性读取
      get(target, prop) {
      console.log('Getting property "￥{prop}"');
      return target[prop];
      },

      // 拦截属性赋值
      set(target, prop, value) {
      console.log('Setting property "￥{prop}" to "￥{value}"');
      target[prop] = value;
      return true; // 表示赋值成功
      }
      };

      const proxy = new Proxy(target, handler);

      // 使用代理对象
      console.log(proxy.name); // 输出: "Getting property "name""，然后返回 "Alice"
      proxy.age = 31; // 输出: "Setting property "age" to "31""
      2. 拦截器（Traps）
      Proxy 支持多种拦截器，覆盖对象的所有基本操作：
      javascript
      const handler = {
      get() {}, // 拦截属性读取
      set() {}, // 拦截属性赋值
      has() {}, // 拦截in操作符
      deleteProperty() {}, // 拦截delete操作
      ownKeys() {}, // 拦截Object.getOwnPropertyNames等
      getOwnPropertyDescriptor() {}, // 拦截Object.getOwnPropertyDescriptor
      defineProperty() {}, // 拦截Object.defineProperty
      setPrototypeOf() {}, // 拦截Object.setPrototypeOf
      getPrototypeOf() {}, // 拦截Object.getPrototypeOf
      isExtensible() {}, // 拦截Object.isExtensible
      preventExtensions() {}, // 拦截Object.preventExtensions
      apply() {}, // 拦截函数调用（当代理目标是函数时）
      construct() {}, // 拦截new操作符（当代理目标是构造函数时）
      };
      二、应用场景
      1. 数据验证与拦截
      场景：在属性赋值时进行验证，确保数据合法性。
      javascript
      const person = { name: '', age: 0 };

      const validator = {
      set(target, prop, value) {
      if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
      }
      if (prop === 'name' && value.length < 2) { throw new Error('Name must be at least 2 characters'); }
        target[prop]=value; return true; } }; const proxyPerson=new Proxy(person, validator); proxyPerson.age='thirty' ;
        // 抛出TypeError proxyPerson.name='A' ; // 抛出Error 2. 私有属性 / 方法 场景：隐藏对象的内部实现，只暴露公共接口。 javascript const
        privateProps=new WeakMap(); class Person { constructor(name, age) { privateProps.set(this, { _name: name, _age:
        age }); return new Proxy(this, { get(target, prop) { if (prop.startsWith('_')) { throw new Error('Private
        property access'); } return privateProps.get(target)[prop]; }, set(target, prop, value) { if
        (prop.startsWith('_')) { throw new Error('Private property access'); } privateProps.get(target)[prop]=value;
        return true; } }); } } const p=new Person('Alice', 30); console.log(p.name); // 'Alice' console.log(p._name); //
        抛出Error 3. 自动填充属性 场景：访问不存在的属性时，自动创建并返回默认值。 javascript const createAutoFillObject=()=> {
        return new Proxy({}, {
        get(target, prop) {
        if (!(prop in target)) {
        target[prop] = {}; // 自动创建嵌套对象
        return target[prop];
        }
        return target[prop];
        }
        });
        };

        const obj = createAutoFillObject();
        obj.a.b.c = 123; // 无需预先定义a和b
        console.log(obj.a.b.c); // 123
        4. 缓存计算结果
        场景：拦截方法调用，缓存计算结果以提高性能。
        javascript
        const cache = new WeakMap();

        function withCache(fn) {
        return new Proxy(fn, {
        apply(target, thisArg, args) {
        const key = args.toString();
        if (!cache.has(key)) {
        cache.set(key, target.apply(thisArg, args));
        }
        return cache.get(key);
        }
        });
        }

        const expensiveFunction = withCache((a, b) => {
        console.log('Performing expensive calculation...');
        return a + b;
        });

        console.log(expensiveFunction(1, 2)); // 执行计算，输出3
        console.log(expensiveFunction(1, 2)); // 直接从缓存返回3
        5. 实现响应式系统
        场景：在数据变化时自动触发更新（如 Vue 3 的响应式原理）。
        javascript
        function reactive(obj) {
        return new Proxy(obj, {
        get(target, prop) {
        track(target, prop); // 收集依赖
        return target[prop];
        },
        set(target, prop, value) {
        target[prop] = value;
        trigger(target, prop); // 触发更新
        return true;
        }
        });
        }

        const state = reactive({ count: 0 });
        // 当state.count变化时，自动更新UI
        6. 函数参数验证
        场景：拦截函数调用，验证参数类型和数量。
        javascript
        function validateParams(schema) {
        return (target) => {
        return new Proxy(target, {
        apply(target, thisArg, args) {
        for (let i = 0; i
        《script schema.length; i++) { if (args[i] && typeof args[i] !==schema[i]) { throw new TypeError('Parameter ￥{ i
          } must be ￥{ schema[i]}'); } } return target.apply(thisArg, args); } }); }; } const add=(a, b)=> a + b;
const validatedAdd = validateParams(['number', 'number'])(add);

validatedAdd(1, 2); // 正常执行
validatedAdd('a', 2); // 抛出TypeError
三、面试答题要点
核心概念：
元编程：拦截并自定义对象的基本操作。
代理对象：与原对象分离，可独立控制访问。
拦截器（Traps）：覆盖多种操作（如 get/set/apply 等）。
与 Object.defineProperty 的对比：
Proxy：拦截所有操作，支持新增属性，更强大灵活。
Object.defineProperty：只能拦截属性的 get/set，需预先定义属性。
应用场景：
数据验证与拦截。
私有属性 / 方法。
自动填充与懒加载。
缓存与性能优化。
响应式系统（如 Vue 3）。
参数验证与函数增强。
性能考量：
Proxy 的拦截操作会引入一定开销，大规模数据操作时需谨慎使用。
适用于需要精细控制对象行为的场景，而非简单数据存储。
四、总结
Proxy 是 ES6 中功能强大的元编程工具，通过拦截对象的基本操作，为开发者提供了前所未有的控制能力。它在数据验证、私有属性、响应式系统等场景中发挥着重要作用，是现代 JavaScript 框架（如 Vue 3）的核心技术之一。理解 Proxy 的工作原理和应用场景，有助于编写更灵活、更健壮的代码。`
              }
            ]
          },
        ]
      },
      {
        tag: `http面试题`,
        data: [
          {
            question: `1.如何理解OSI七层模型？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，OSI 七层模型是计算机网络的基础概念，理解它有助于解释网络数据的传输流程、排查前后端通信问题（如跨域、请求失败等）。以下是对 OSI 七层模型的详细解析：
一、OSI 七层模型的核心概念
OSI（Open Systems Interconnection，开放系统互连）七层模型是国际标准化组织（ISO）提出的网络通信框架，它将网络通信的过程划分为7 个层次，每层负责特定的功能，层与层之间通过接口交互，最终实现不同设备之间的标准化通信。
核心思想：分层隔离—— 每一层只关注自身职责，通过 “封装”（向下传递时添加本层协议头）和 “解封装”（向上传递时移除本层协议头）完成数据传输，降低网络通信的复杂度。
二、七层模型从下到上的具体含义与功能
按从底层到高层（物理层→应用层）排序，每层的作用如下：
物理层（Physical Layer）
功能：负责将数据转换为电信号、光信号或无线电信号等物理形式，通过物理介质（如网线、光纤、无线电磁波）传输。
关键：只关注 “信号的传输”，不关心数据含义，涉及硬件接口（如网线接口、无线网卡）、传输速率（如 100Mbps）、信号编码方式等。
举例：网线、光纤、网卡的物理连接，集线器（Hub）工作在此层。
数据链路层（Data Link Layer）
功能：在物理层基础上，确保相邻设备（如两台直接连接的电脑）之间的可靠传输，解决 “信号传输中的错误” 和 “数据帧的边界识别”。
关键：
定义 “数据帧”（Frame）格式，添加 MAC 地址（设备的物理地址）作为发送方和接收方标识；
通过校验码（如 CRC）检测数据传输错误，并重传错误帧。
举例：交换机（Switch）工作在此层，MAC 地址是该层的核心标识。
网络层（Network Layer）
功能：实现跨网络的数据包路由与转发，解决 “不同网络之间的通信”（如局域网到互联网）。
关键：
定义 “数据包”（Packet）格式，添加 IP 地址（逻辑地址，如 192.168.1.1）作为源和目标地址；
通过路由算法（如 OSPF、RIP）选择最佳传输路径。
举例：路由器（Router）工作在此层，IP 协议（IPv4/IPv6）是该层的核心协议。
传输层（Transport Layer）
功能：负责端到端（如客户端到服务器）的可靠数据传输，确保数据完整、有序到达。
关键：
提供两种传输方式：
TCP（传输控制协议）：面向连接、可靠传输（通过三次握手建立连接、重传丢失数据、按序重组），适用于对可靠性要求高的场景（如 HTTP 请求、文件传输）；
UDP（用户数据报协议）：无连接、不可靠传输（速度快、开销小），适用于实时性要求高的场景（如视频通话、游戏）；
通过 “端口号” 区分同一设备上的不同应用（如 HTTP 默认 80 端口，HTTPS 默认 443 端口）。
举例：TCP/UDP 协议，前端发起的 API 请求依赖 TCP 建立连接。
会话层（Session Layer）
功能：管理通信双方的 “会话连接”，包括建立、维持、终止会话（如登录状态的保持）。
关键：解决 “长时间通信中的连接复用”，例如 Web 开发中的 Cookie、Session 机制本质上是会话层功能的体现（实际 TCP/IP 协议中未明确划分此层，功能常被合并到应用层）。
举例：登录状态的维持、断点续传的会话管理。
表示层（Presentation Layer）
功能：负责数据的 “格式转换” 和 “加密解密”，确保接收方能够理解发送方的数据格式。
关键：处理数据的编码、解码、压缩、解压、加密、解密等，统一不同系统的数据格式。
举例：
数据格式转换（如 JSON 与二进制的转换）；
加密（如 HTTPS 的 SSL/TLS 加密）；
压缩（如 HTTP 请求中的 gzip 压缩）。
应用层（Application Layer）
功能：直接为用户应用程序提供网络服务，是用户能直接感知的层。
关键：定义应用程序之间的通信协议，满足具体业务需求。
举例：前端常用的 HTTP（网页传输）、HTTPS（加密的 HTTP）、WebSocket（实时通信）、FTP（文件传输）等协议，均工作在此层。
三、数据传输流程（以 “前端发送 HTTP 请求” 为例）
应用层：浏览器（应用程序）生成 HTTP 请求报文（如GET /api/data）；
表示层：对 HTTP 报文进行压缩（如 gzip）或加密（如 HTTPS）；
会话层：维持浏览器与服务器的会话（如携带 Cookie）；
传输层：将数据分割为 TCP 数据包，添加源端口（如浏览器随机端口）和目标端口（如 443）；
网络层：为 TCP 数据包添加源 IP（客户端 IP）和目标 IP（服务器 IP），通过路由选择传输路径；
数据链路层：为 IP 数据包添加 MAC 地址（相邻设备的物理地址），封装为数据帧；
物理层：将数据帧转换为电信号 / 光信号，通过网线 / 光纤传输；
服务器端按相反流程 “解封装”，最终将请求传递到应用层处理，再按同样流程返回响应。
四、前端视角的重要性
前端开发中，虽然不需要深入底层细节，但理解 OSI 模型有助于：
解释HTTP 请求的传输过程（如 “为什么跨域是应用层的限制”“HTTPS 的加密发生在表示层”）；
排查问题（如 “请求超时可能是传输层 TCP 连接失败”“跨域错误是应用层 HTTP 协议的限制”）；
理解实时通信技术（如 WebSocket 基于 TCP，属于应用层协议）。
总结：OSI 七层模型是网络通信的 “说明书”，分层思想是理解所有网络协议的基础，前端开发者需重点掌握应用层（HTTP/HTTPS）、传输层（TCP/UDP）、网络层（IP） 的功能，因为它们直接影响前端与后端的通信逻辑。`
              }
            ]
          },
          {
            question: `2. 如何理解TCP/IP协议?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，TCP/IP 协议是网络通信的核心考点，理解它有助于解释 HTTP 请求的底层原理、排查网络故障（如请求超时、跨域）等问题。以下是对 TCP/IP 协议的详细解析：
一、TCP/IP 协议的核心概念
TCP/IP（Transmission Control Protocol/Internet Protocol）是互联网通信的基础协议栈，它将网络通信划分为4 层（相比 OSI 的 7 层更简洁实用），每层负责特定的功能，通过 “封装” 和 “解封装” 实现数据的跨网络传输。
核心思想：分层协作—— 各层独立开发、互不依赖，通过接口传递数据，最终实现 “不同设备间的标准化通信”。
二、TCP/IP 四层模型与对应协议
按从底层到高层排序，每层的作用及典型协议如下：
网络接口层（Network Interface Layer）
功能：负责物理网络的连接，处理与硬件相关的细节（如网卡驱动、物理介质）。
对应 OSI 层：物理层 + 数据链路层。
关键协议：以太网协议、Wi-Fi 协议、ARP（地址解析协议，将 IP 转换为 MAC 地址）。
举例：网线、交换机工作在此层。
网络层（Internet Layer）
功能：实现跨网络的数据包路由与转发，解决 “不同网络之间的通信”。
对应 OSI 层：网络层。
关键协议：
IP 协议（IPv4/IPv6）：为数据包添加源 IP 和目标 IP 地址，实现路由寻址；
ICMP 协议：网络控制消息协议（如 Ping 命令基于此协议）；
ARP/RARP：地址解析协议（IP ↔ MAC 地址转换）。
举例：路由器工作在此层，IP 地址是该层的核心标识。
传输层（Transport Layer）
功能：负责端到端的可靠数据传输，确保数据完整、有序到达。
对应 OSI 层：传输层。
关键协议：
TCP 协议（传输控制协议）：面向连接、可靠传输，提供以下机制：
三次握手（Three-Way Handshake）：建立连接前确认双方通信能力；
数据分段：将大文件分割为小数据包；
序列号与确认应答：确保数据按序到达，丢失时重传；
滑动窗口：流量控制，避免发送方过快导致接收方溢出；
拥塞控制：根据网络拥塞情况动态调整发送速率。
UDP 协议（用户数据报协议）：无连接、不可靠传输，但开销小、速度快，适用于实时性要求高的场景（如视频通话、游戏）。
举例：HTTP 协议默认基于 TCP 传输，WebSocket 也是基于 TCP 的应用层协议。
应用层（Application Layer）
功能：直接为用户应用程序提供网络服务，定义应用程序之间的通信规则。
对应 OSI 层：会话层 + 表示层 + 应用层。
关键协议：
HTTP/HTTPS：网页传输协议（前端最常用）；
WebSocket：双向实时通信协议；
FTP：文件传输协议；
SMTP/POP3/IMAP：邮件传输协议；
DNS：域名系统（将域名解析为 IP 地址）。
举例：前端发送的 AJAX 请求、WebSocket 连接均基于应用层协议。
三、TCP 协议的核心机制
TCP 是互联网中最常用的传输协议，其核心特性包括：
三次握手（Three-Way Handshake）
目的：建立可靠连接，确认双方发送和接收能力。
流程：
客户端 → 服务器：发送 SYN 包（请求建立连接，携带初始序列号 x）；
服务器 → 客户端：发送 SYN+ACK 包（同意建立连接，携带自身初始序列号 y 和对 x 的确认）；
客户端 → 服务器：发送 ACK 包（确认收到服务器的 SYN+ACK，携带对 y 的确认）。
前端影响：HTTP 请求前必须先完成 TCP 三次握手，因此 HTTPS 请求耗时更长（需额外的 TLS 握手）。
四次挥手（Four-Way Handshake）
目的：优雅地关闭 TCP 连接。
流程：
客户端 → 服务器：发送 FIN 包（请求关闭连接）；
服务器 → 客户端：发送 ACK 包（确认收到 FIN）；
服务器 → 客户端：发送 FIN 包（服务器也请求关闭连接）；
客户端 → 服务器：发送 ACK 包（确认收到服务器的 FIN）。
前端影响：长连接（如 WebSocket）需妥善处理四次挥手，避免资源泄漏。
可靠传输机制
序列号（Sequence Number）：为每个数据包编号，确保接收方按序重组；
确认应答（ACK）：接收方收到数据后返回 ACK，发送方未收到则重传；
超时重传（Retransmission）：发送方在一定时间内未收到 ACK，自动重发数据；
滑动窗口（Sliding Window）：动态调整发送方和接收方的缓冲区大小，实现流量控制。
四、UDP 协议的特点
UDP 与 TCP 的对比：
特性	TCP	UDP
连接方式	面向连接（需三次握手）	无连接（直接发送）
可靠性	可靠（通过序列号、ACK、重传）	不可靠（可能丢包、乱序）
传输效率	低（开销大）	高（开销小）
应用场景	HTTP、文件传输、邮件等	视频通话、游戏、实时监控等
首部开销	20 字节	8 字节
五、前端视角的重要性
前端开发中，TCP/IP 协议的理解直接影响：
HTTP 请求的性能优化
TCP 握手耗时：减少 HTTP 请求数量（如合并 CSS/JS）、使用 HTTP/2（复用 TCP 连接）；
TCP 慢启动：避免频繁建立新连接（如使用 Keep-Alive）。
实时通信技术选择
WebSocket 基于 TCP，适合高可靠性场景（如聊天、实时数据推送）；
WebRTC 基于 UDP，适合低延迟场景（如视频会议）。
网络故障排查
请求超时：可能是 TCP 连接建立失败或丢包；
跨域问题：本质是应用层 HTTP 协议的限制，与 TCP/IP 无关。
安全理解
HTTPS 在 TCP 基础上加入 TLS 加密（属于应用层），确保数据传输安全。
六、常见面试问题
TCP 为什么需要三次握手？两次不行吗？
答：两次握手无法确保双方的发送和接收能力都正常。例如，客户端发送的第一个 SYN 包可能因网络延迟而在连接关闭后才到达服务器，若采用两次握手，服务器会误认为是新连接并同意，导致资源浪费。
TCP 和 UDP 的区别？
答：TCP 可靠、面向连接、开销大；UDP 不可靠、无连接、开销小。TCP 适用于对可靠性要求高的场景，UDP 适用于对实时性要求高的场景。
如何理解 “TCP 粘包” 问题？
答：TCP 是流协议，无消息边界。当发送多个小数据包时，接收方可能一次性收到多个包（粘包）或一个包被拆分（半包）。解决方法是在应用层定义消息边界（如添加长度字段、特殊分隔符）。
为什么 HTTP/2 比 HTTP/1.1 快？
答：HTTP/2 基于 TCP，复用同一个连接，支持多路复用（多个请求并行），减少了 TCP 握手和慢启动的开销。
七、总结
TCP/IP 协议是互联网的基石，前端开发者需重点掌握：
四层模型：网络接口层、网络层（IP）、传输层（TCP/UDP）、应用层（HTTP 等）；
TCP 的核心机制：三次握手、四次挥手、可靠传输；
TCP 与 UDP 的适用场景：根据业务需求选择合适的传输协议；
对前端开发的影响：优化 HTTP 请求、理解实时通信原理、排查网络故障。`
              }
            ]
          },
          {
            question: `3.如何理解UDP和TCP?区别？应用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `如何理解 UDP 和 TCP？区别？应用场景？
在前端面试中，UDP 和 TCP 是网络协议部分的高频考点，二者同属于 TCP/IP 协议族的传输层协议，负责数据在网络中的可靠传输，但设计理念和适用场景差异显著。
一、TCP 与 UDP 的核心概念
TCP（Transmission Control Protocol，传输控制协议）
一种面向连接、可靠的、基于字节流的传输层协议。
核心特点：通过三次握手建立连接、四次挥手断开连接，使用确认机制、重传机制、流量控制、拥塞控制等确保数据无丢失、无重复、按序到达。
UDP（User Datagram Protocol，用户数据报协议）
一种无连接、不可靠、基于数据报的传输层协议。
核心特点：无需建立连接，直接发送数据，不保证数据到达的顺序和完整性，仅提供简单的校验机制，传输效率更高。
二、TCP 与 UDP 的核心区别
对比维度	TCP	UDP
连接性	面向连接（需三次握手建立连接）	无连接（直接发送数据，无需建立连接）
可靠性	可靠（通过确认、重传、排序等机制保证）	不可靠（不保证数据到达，可能丢失或乱序）
传输方式	字节流（数据被视为连续的字节序列）	数据报（每个数据包独立，有固定大小限制）
效率	较低（连接建立 / 断开、确认机制消耗资源）	较高（无额外开销，传输速度快）
头部开销	较大（头部约 20-60 字节）	较小（头部固定 8 字节）
适用场景	对可靠性要求高的场景（如文件传输、登录）	对实时性要求高的场景（如视频通话、游戏）
三、应用场景
TCP 的典型应用场景
需保证数据完整、有序的场景，例如：
HTTP/HTTPS 协议（网页加载、表单提交等，需确保数据准确）；
文件传输（如 FTP 协议，需保证文件无损坏）；
邮件传输（如 SMTP 协议，需确保邮件完整送达）；
登录 / 支付等关键操作（需验证数据的准确性和安全性）。
UDP 的典型应用场景
需优先保证实时性，可容忍少量数据丢失的场景，例如：
视频 / 语音通话（如 Zoom、微信通话，卡顿比延迟更影响体验，少量丢包可通过算法弥补）；
实时游戏（如王者荣耀、FPS 游戏，低延迟比偶尔丢包更重要）；
广播 / 多播（如直播弹幕、物联网设备通信，无需与每个接收方建立连接）；
DNS 查询（域名解析需快速响应，即使偶尔失败可重试，效率优先）。
四、前端视角的理解
前端开发中，虽然不直接操作 TCP/UDP 协议，但需理解其底层原理对应用的影响：
日常使用的AJAX、WebSocket（基于 TCP） 依赖 TCP 的可靠性，确保接口数据准确传输；
实时音视频、在线游戏等场景可能基于WebRTC（底层使用 UDP），通过牺牲部分可靠性换取低延迟；
理解二者区别有助于排查网络问题（如 TCP 重传导致的延迟、UDP 丢包导致的画面卡顿）。
总结：TCP 是 “稳扎稳打” 的可靠传输，UDP 是 “追求速度” 的轻量传输，二者互补，共同支撑网络通信的多样性需求。`
              }
            ]
          },
          {
            question: `4.说一下GET和POST 的区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，GET 和 POST 是 HTTP 协议中最基础且高频的考点，二者本质都是 HTTP 请求方法，但设计目标和应用场景存在显著差异。以下是从多个维度的详细对比：
一、核心区别对比表
对比维度	GET	POST
语义	获取资源（如查询数据）	提交数据（如表单提交、创建资源）
参数位置	参数附加在 URL 的query string中（如?name=value）	参数放在请求体（body）中
安全性	参数暴露在 URL 中，不适合传输敏感信息（如密码）	参数在请求体中，相对更安全（但仍需 HTTPS 加密）
幂等性	幂等（多次请求结果相同，如查询操作）	非幂等（多次提交可能创建多条数据，如表单重复提交）
缓存	默认被浏览器缓存（如 Chrome 的 200 状态 GET 请求会被缓存）	默认不缓存
编码类型	仅支持application/x-www-form-urlencoded（URL 编码）	支持多种编码类型（如application/x-www-form-urlencoded、multipart/form-data、JSON）
参数长度	受浏览器和服务器限制（如 Chrome 约 8182 字节）	理论无限制（实际受服务器配置限制，如 Nginx 默认 1MB）
应用场景	查询数据（如搜索、分页）	提交数据（如登录、注册、上传文件）
二、深入理解差异点
1. 语义与设计初衷
GET：设计用于 “获取资源”，请求应是只读操作，不修改服务器数据。
例如：GET /api/users/1 获取 ID 为 1 的用户信息。
POST：设计用于 “提交数据”，可能对服务器数据产生副作用（如创建、修改）。
例如：POST /api/users 创建新用户。
2. 参数位置与安全性
GET：参数直接拼接到 URL 中，例如：
plaintext
GET /search?keyword=apple&page=1 HTTP/1.1
Host: example.com

缺点：
参数会被浏览器历史记录、服务器日志保存，敏感信息（如密码）易泄露；
URL 长度有限制，超出会导致请求失败。
POST：参数放在请求体中，例如：
http
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

优点：参数不直接暴露，但仍需 HTTPS 加密防止中间人攻击。
3. 幂等性（Idempotence）
GET：幂等。多次相同请求应返回相同结果，例如多次刷新页面不应创建新数据。
POST：非幂等。例如多次提交表单可能创建多条记录，需前端防重（如按钮禁用）或后端去重（如唯一 ID）。
4. 缓存机制
GET：默认被浏览器缓存，例如：
javascript
// 第一次请求会发送到服务器
fetch('/api/data');
// 短时间内相同URL的请求可能直接从缓存返回
fetch('/api/data');

可通过添加随机参数（如时间戳）禁用缓存：
javascript
fetch('/ api / data ? t = ￥{ Date.now() }');

POST：默认不缓存，如需缓存需手动配置（如使用 Service Worker）。
5. 编码类型（Content-Type）
GET：仅支持application/x-www-form-urlencoded，参数格式为key1=value1&key2=value2。
POST：支持多种编码类型：
application/x-www-form-urlencoded（默认）：同 GET，如表单提交；
multipart/form-data：文件上传时使用；
application/json：前后端分离项目中最常用，如：
javascript
fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice', age: 30 })
});

三、常见面试问题
GET 请求可以有 body 吗？
技术上可行，但 HTTP 规范未明确禁止，但服务器通常会忽略 GET 的 body。
实际场景中，GET 的参数必须放在 URL 的 query string 中。
POST 比 GET 更安全吗？
相对更安全（参数不在 URL 中），但仍需 HTTPS 加密。
即使使用 POST，敏感数据（如密码）也不应明文传输。
GET 和 POST 的参数长度限制从何而来？
GET：受浏览器（如 Chrome 约 8182 字节）和服务器（如 Nginx 默认无限制）限制；
POST：受服务器配置限制（如 Nginx 默认 1MB，可通过client_max_body_size调整）。
什么情况下 GET 请求会修改服务器数据？
违反语义的错误实现（如在 GET 请求中调用数据库写操作），但这是不良实践。
正确做法：修改数据必须使用 POST/PUT/DELETE 等方法。
四、总结
GET 和 POST 的选择应基于语义而非 “安全性” 或 “长度限制”：
GET：用于获取数据，参数简单且需缓存时优先选择；
POST：用于提交数据，参数复杂、包含文件或需防重复时使用。
在实际开发中，无论使用哪种方法，涉及敏感数据时都必须配合 HTTPS 加密，确保数据传输安全。`
              }
            ]
          },
          {
            question: `5.说说TCP为什么需要三次握手和四次挥手？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，TCP 的三次握手和四次挥手是考察网络基础的高频考点，核心是理解 TCP 作为可靠传输协议的设计逻辑 —— 通过 “握手” 建立可靠连接，通过 “挥手” 安全终止连接。以下从原理、目的和细节展开说明：
一、TCP 三次握手：建立连接的必要性
TCP 是面向连接的协议，通信前必须先建立连接（类似打电话 “喂→听到了→好的，开始说” 的确认过程）。三次握手的核心目的是：确保双方的发送和接收能力都正常，并协商初始序列号（ISN）。
1. 三次握手的过程（简化版）
第一次握手：客户端 → 服务器
发送 SYN 报文（同步请求），携带客户端初始序列号 ISN(c)，表示 “我想连接你，我的序列号从 ISN (c) 开始”。
此时客户端状态：SYN-SENT（等待服务器确认）。
第二次握手：服务器 → 客户端
发送 SYN+ACK 报文（同步 + 确认），携带服务器初始序列号 ISN(s) 和确认号 ISN(c)+1，表示 “我收到你的请求了，我的序列号从 ISN (s) 开始，你可以发下一个数据了”。
此时服务器状态：SYN-RCVD（等待客户端确认）。
第三次握手：客户端 → 服务器
发送 ACK 报文（确认），携带确认号 ISN(s)+1，表示 “我收到你的回应了，连接建立成功”。
此时双方状态：ESTABLISHED（可开始通信）。
2. 为什么需要三次？而不是两次或四次？
排除两次握手：
若仅两次握手，服务器无法确认客户端是否能收到自己的 SYN+ACK。
例如：客户端的第一个 SYN 报文因网络延迟变成 “失效报文”，服务器收到后回复 SYN+ACK，但客户端已超时重发并建立新连接，此时服务器会一直等待客户端确认，造成资源浪费。
三次握手的第三次 ACK 能让服务器确认 “客户端已就绪”，避免无效连接。
无需四次握手：
第二次握手将 SYN（服务器同步）和 ACK（确认客户端）合并为一个报文，减少一次通信，提高效率。三次已能满足 “双方收发能力均正常” 的验证，四次多余。
二、TCP 四次挥手：终止连接的必要性
TCP 连接是全双工（双方可同时发送数据），终止连接时需双方分别确认 “不再发送数据”，因此需要四次挥手（类似打电话 “我说完了→知道了→我也说完了→好的，挂了”）。
1. 四次挥手的过程（简化版）
第一次挥手：客户端 → 服务器
发送 FIN 报文（结束请求），表示 “我没有数据要发了”。
此时客户端状态：FIN-WAIT-1（等待服务器确认）。
第二次挥手：服务器 → 客户端
发送 ACK 报文，确认收到客户端的 FIN，表示 “知道你说完了，但我可能还有数据没发完，你等一下”。
此时客户端状态：FIN-WAIT-2（等待服务器发完数据）；服务器状态：CLOSE-WAIT（准备关闭）。
第三次挥手：服务器 → 客户端
服务器数据发送完毕，发送 FIN 报文，表示 “我也没有数据要发了”。
此时服务器状态：LAST-ACK（等待客户端确认）。
第四次挥手：客户端 → 服务器
发送 ACK 报文，确认收到服务器的 FIN，表示 “知道了，你可以关闭了”。
此时客户端状态：TIME-WAIT（等待 2MSL 后关闭），服务器收到后直接关闭。
2. 为什么需要四次？而不是三次？
核心原因：TCP 是全双工通信，双方需各自独立终止发送能力。
第二次挥手（服务器的 ACK）和第三次挥手（服务器的 FIN）无法合并：
服务器收到客户端的 FIN 后，可能还在处理数据（如继续发送未完成的响应），不能立即发送 FIN，因此必须分两次回复 —— 先确认 “收到关闭请求”，再在数据发完后主动发起 “关闭通知”。
因此，四次挥手是全双工特性的必然结果。
TIME-WAIT 状态的意义：
客户端最后发送 ACK 后，需等待 2MSL（报文最大生存时间，通常 1-2 分钟），目的是：
确保服务器能收到最后一个 ACK（若服务器没收到，会重发 FIN，客户端可在 TIME-WAIT 内再次回复）；
避免已失效的报文干扰新连接（2MSL 内旧报文会自然消失）。
三、面试高频问题总结
三次握手的核心目的：
确认双方收发能力正常，协商初始序列号（ISN），防止无效连接。
四次挥手的核心目的：
全双工通信中，双方需各自确认 “数据发送完毕”，确保连接终止前数据不丢失。
为什么三次握手不能少：
两次握手无法让服务器确认客户端能收到自己的回应，可能导致资源泄露。
TIME-WAIT 状态的作用：
保证最后一个 ACK 被服务器收到，避免旧报文干扰新连接。
总结
TCP 的三次握手和四次挥手，本质是可靠传输协议的设计体现—— 通过严谨的确认机制，在不可靠的网络中实现数据的安全传输和连接管理。理解这一过程，能帮助前端开发者更深入地理解 HTTP 请求（基于 TCP）的底层原理，例如：为什么网络波动时请求可能超时（握手失败）、为什么关闭页面后仍能收到部分响应（挥手前数据已发送）等现象。`
              }
            ]
          },
          {
            question: `6.说说HTTP常见的请求头有哪些？作用？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端开发中，HTTP 请求头是客户端（浏览器）向服务器发送请求时附加的元数据，用于描述请求的相关信息（如数据类型、身份验证、缓存策略等）。掌握常见请求头的作用，有助于理解前后端交互的细节（如跨域、缓存控制、数据格式协商等）。以下是面试高频的请求头及其作用：
一、基础信息类请求头
用于标识请求的基本属性（如 URL、客户端信息等）。
Host
作用：指定服务器的域名和端口号（默认 80/443 可省略）。
示例：Host: www.example.com 或 Host: api.example.com:8080
说明：HTTP/1.1 要求必须包含此头，用于服务器在多个虚拟主机（共享 IP）时区分请求目标。
User-Agent（UA）
作用：标识发送请求的客户端（浏览器、设备、爬虫等）信息。
示例：User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
说明：服务器可通过 UA 判断客户端类型（如移动端 / PC 端），返回适配内容；也用于反爬虫（识别非浏览器请求）。
Referer
作用：记录当前请求的来源页面 URL（即 “从哪个页面跳过来的”）。
示例：Referer: https://www.google.com/search?q=example
说明：用于统计来源、防盗链（如图片服务器验证 Referer 是否为允许的域名）；注意：从浏览器地址栏直接输入的请求，Referer 为空。
二、数据传输类请求头
用于描述请求体的格式、编码等，前端发送 POST/PUT 等请求时常用。
Content-Type
作用：指定请求体的数据格式（MIME 类型），服务器据此解析数据。
常见值：
application/json：JSON 格式（前端接口请求最常用，如fetch或axios默认）；
application/x-www-form-urlencoded：表单数据（key=value&key2=value2，传统表单提交默认）；
multipart/form-data：文件上传（配合<input type="file">，支持二进制数据）；
text/plain：纯文本格式。
Content-Length
作用：表示请求体的字节长度，服务器据此判断数据是否接收完整。
示例：Content-Length: 1024（请求体大小为 1024 字节）。
Accept-Encoding
作用：告知服务器客户端支持的压缩算法，服务器可据此压缩响应数据（减少传输量）。
常见值：gzip, deflate, br（分别对应 gzip、deflate、Brotli 压缩）。
三、缓存控制类请求头
用于控制浏览器和服务器的缓存策略，优化性能（前端性能优化重点）。
Cache-Control
作用：请求时告知服务器客户端的缓存偏好（如是否使用缓存、缓存有效期等）。
常见值：
no-cache：不直接使用缓存，需向服务器验证缓存是否有效（协商缓存）；
no-store：完全不缓存，每次都请求新数据（如支付页面）；
max-age=0：缓存已过期，需重新请求。
If-Modified-Since
作用：配合服务器的Last-Modified响应头，实现协商缓存。
逻辑：客户端发送此头（值为上次响应的Last-Modified），服务器判断资源是否在该时间后修改：
未修改：返回 304 Not Modified（不返回数据，用缓存）；
已修改：返回 200 OK 和新数据。
If-None-Match
作用：配合服务器的ETag响应头，实现更精确的协商缓存（比If-Modified-Since优先级高）。
逻辑：客户端发送此头（值为上次响应的ETag，资源唯一标识），服务器对比当前资源的ETag：
一致：返回 304（用缓存）；
不一致：返回 200 和新数据。
四、身份验证与安全类请求头
用于身份验证、跨域请求等场景。
Authorization
作用：携带身份验证信息（如 Token、用户名密码），用于服务器验证客户端权限。
常见格式：
Bearer <token>：JWT 令牌（前端登录后常用，如Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...）；
Basic <base64编码的用户名:密码>：基础认证（安全性低，少用）。
Origin
作用：在跨域请求（如 CORS）中，标识请求的来源域名（仅包含协议 + 域名 + 端口，无路径）。
示例：Origin: https://www.baidu.com
说明：服务器通过Origin判断是否允许跨域请求（配合Access-Control-Allow-Origin响应头）。
Cookie
作用：携带服务器之前通过Set-Cookie设置的 Cookie 信息（键值对），用于保持会话状态（如登录状态）。
示例：Cookie: sessionId=abc123; username=test
说明：受同源策略限制，仅向同源服务器发送对应 Cookie。
五、其他高频请求头
Accept
作用：告知服务器客户端可接收的响应数据格式（MIME 类型），服务器据此返回最合适的格式。
示例：Accept: application/json, text/plain, */*（优先 JSON，其次纯文本，最后任意类型）。
Connection
作用：控制连接是否保持持久化（HTTP/1.1 默认长连接）。
常见值：keep-alive（保持连接，避免频繁建立 TCP 连接）、close（请求后关闭连接）。
Range
作用：用于断点续传或分片下载，指定请求资源的部分字节范围。
示例：Range: bytes=0-499（请求资源的前 500 字节），服务器成功则返回 206 Partial Content。
面试总结
前端开发中，需重点掌握与前后端交互、缓存、跨域、身份验证相关的请求头，例如：
发送 JSON 数据时，Content-Type: application/json是必设的；
跨域请求中，Origin和Authorization（带 Token 时）需要服务器在 CORS 中允许；
缓存优化中，Cache-Control、If-Modified-Since、If-None-Match是核心工具。
理解这些请求头的作用，能帮助排查接口错误（如 415 Unsupported Media Type 可能是Content-Type错误）、优化请求性能（如合理利用缓存）。`
              }
            ]
          },
          {
            question: `7.说说HTTP常见的状态码有哪些，适用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端开发中，HTTP 状态码是服务器对客户端请求的响应状态标识，掌握常见状态码的含义和适用场景，能快速定位前后端交互问题（如接口报错、资源加载失败等）。以下是面试高频的状态码分类及适用场景：
一、信息类状态码（1xx）
表示服务器已接收请求，正在处理，前端较少直接关注。
100 Continue
作用：服务器确认已接收请求头，允许客户端继续发送请求体（常用于大文件上传）。
场景：客户端发送Expect: 100-continue请求头后，服务器返回此状态码，示意 “可以继续发送数据”。
二、成功类状态码（2xx）
表示请求被服务器成功处理。
200 OK
作用：请求成功，服务器返回对应数据（最常见的成功状态）。
场景：GET 请求获取数据（如列表查询）、POST 请求提交成功（如表单提交返回结果）等。
201 Created
作用：请求成功且服务器创建了新资源（如新增数据）。
场景：POST 请求创建资源（如注册新用户、上传新文件），响应体通常包含新资源的 URL 或详情。
204 No Content
作用：请求成功，但服务器无数据返回（仅返回响应头）。
场景：DELETE 请求删除资源（服务器确认删除成功，但无需返回内容）、PUT 请求更新资源（仅确认成功，无额外数据）。
206 Partial Content
作用：服务器成功处理了部分请求（用于断点续传或分片下载）。
场景：客户端通过Range请求头指定资源的部分字节范围（如视频分片加载），服务器返回对应片段。
三、重定向类状态码（3xx）
表示请求需要客户端进一步操作（如跳转）才能完成。
301 Moved Permanently
作用：资源永久迁移到新 URL，浏览器会缓存新地址，后续请求自动跳转。
场景：网站域名变更（如old.com迁移到new.com），服务器返回新域名 URL，浏览器下次直接访问新地址。
302 Found
作用：资源临时迁移到新 URL，浏览器不缓存新地址，每次请求仍需经过原地址跳转。
场景：临时跳转（如登录后跳回原页面）、活动页临时切换等。
注意：HTTP/1.1 中 303（See Other）更推荐用于 POST 请求后的跳转（避免表单重复提交），但实际中 302 仍常用。
304 Not Modified
作用：协商缓存命中，资源未修改，服务器不返回数据，客户端直接使用本地缓存。
场景：前端优化核心（如静态资源 CSS/JS/ 图片），通过If-Modified-Since或If-None-Match验证缓存有效性，减少数据传输。
307 Temporary Redirect
作用：与 302 类似，但严格遵守请求方法（如 POST 请求跳转后仍用 POST），而 302 可能被浏览器改为 GET。
场景：需要保持请求方法的临时跳转（如 API 临时迁移，需确保 POST 数据不丢失）。
四、客户端错误类状态码（4xx）
表示客户端请求存在错误（如参数错误、权限不足）。
400 Bad Request
作用：服务器无法理解请求（语法错误或参数无效）。
场景：请求体格式错误（如 JSON 语法错误）、必填参数缺失、参数类型错误（如数字字段传字符串）。
401 Unauthorized
作用：请求需要身份验证（用户未登录或 Token 无效）。
场景：未登录状态访问需授权的接口（如个人中心数据）、Token 过期或伪造，前端通常需跳转登录页。
注意：与 403 的区别 ——401 是 “未认证”（缺登录），403 是 “已认证但无权限”。
403 Forbidden
作用：服务器拒绝请求（用户已登录，但无访问权限）。
场景：普通用户访问管理员接口、IP 被拉黑后请求资源。
404 Not Found
作用：请求的资源不存在（URL 错误或资源已删除）。
场景：前端路由错误（如访问/xxx但无对应页面）、接口路径写错（如/api/user写成/api/users）。
405 Method Not Allowed
作用：请求方法（GET/POST 等）不被服务器允许。
场景：用 POST 请求访问只支持 GET 的接口（如/api/list仅允许 GET，但前端用了 POST），响应头Allow会说明支持的方法。
408 Request Timeout
作用：服务器等待请求超时（客户端未在规定时间内发送完整请求）。
场景：网络缓慢时，客户端发送请求不完整，服务器超时后返回，前端可重试请求。
413 Payload Too Large
作用：请求体过大，服务器拒绝处理。
场景：上传文件超过服务器限制（如服务器限制单文件 500MB，前端传了 1GB 文件）。
415 Unsupported Media Type
作用：服务器不支持请求体的格式（Content-Type错误）。
场景：前端发送 JSON 数据但Content-Type写成application/x-www-form-urlencoded，或上传的文件类型不被允许。
429 Too Many Requests
作用：客户端请求过于频繁，触发服务器限流策略。
场景：前端短时间内多次调用接口（如未做防抖的搜索输入），服务器返回此状态，前端需限制请求频率。
五、服务器错误类状态码（5xx）
表示服务器处理请求时发生内部错误（前端无法直接解决，需反馈后端）。
500 Internal Server Error
作用：服务器内部错误（如代码 bug、数据库异常）。
场景：接口逻辑报错（如后端代码抛出未捕获的异常）、数据库连接失败。
502 Bad Gateway
作用：服务器作为网关 / 代理时，收到上游服务器的无效响应。
场景：Nginx 反向代理到后端服务，但后端服务崩溃，Nginx 返回 502。
503 Service Unavailable
作用：服务器暂时无法处理请求（如维护中、负载过高）。
场景：网站正在停机维护、服务器并发量超过上限，响应头Retry-After可能提示重试时间。
504 Gateway Timeout
作用：服务器作为网关 / 代理时，等待上游服务器响应超时。
场景：后端接口处理时间过长（如复杂查询），Nginx 等代理服务器超时返回 504。
面试总结
前端需重点关注2xx（成功）、4xx（客户端错误）、5xx（服务器错误），尤其是：
400/404/415：多为前端请求参数或路径错误，需检查代码；
401/403：与登录状态和权限相关，需处理跳转登录或提示无权限；
304：缓存优化核心，需理解协商缓存逻辑；
5xx：后端问题，前端需记录错误并提示用户重试。
记住常见状态码的含义，能在调试接口时快速定位问题（如看到 415 就检查Content-Type，看到 404 就核对 URL）。`
              }
            ]
          },
          {
            question: `8.什么是HTTP?HTTP和HTTPS的区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端开发中，HTTP 和 HTTPS 是浏览器与服务器通信的核心协议，理解它们的定义和区别是面试高频考点，尤其涉及网络安全和数据传输场景。
一、什么是 HTTP？
HTTP（HyperText Transfer Protocol，超文本传输协议）是一种基于 TCP/IP 的应用层协议，用于规定客户端（如浏览器）和服务器之间的数据传输格式和规则。
核心作用：实现超文本（如 HTML、图片、视频等）的传输，是 WWW（万维网）的基础。
特点：
无状态：服务器不保留客户端的历史请求信息（需通过 Cookie、Session 等机制补充状态）。
明文传输：数据在传输过程中以明文形式发送，易被拦截和篡改。
默认端口：80。
二、HTTP 和 HTTPS 的核心区别
HTTPS（HyperText Transfer Protocol Secure）是 HTTP 的安全版本，在 HTTP 基础上加入了SSL/TLS 加密层，核心区别体现在安全性和传输流程上，具体如下：
对比维度	HTTP	HTTPS
安全性	明文传输，数据易被窃听、篡改、伪造	基于 SSL/TLS 加密，数据传输过程私密且完整
加密机制	无加密	采用对称加密（传输数据）+ 非对称加密（交换密钥）
端口	默认 80	默认 443
证书	无需证书	需向 CA（证书颁发机构）申请 SSL 证书（部分免费，如 Let’s Encrypt）
性能	传输速度快（无加密开销）	速度稍慢（需额外的 SSL 握手和加密计算）
状态标识	浏览器地址栏无特殊标识	地址栏显示锁形图标，部分浏览器显示 “安全” 字样
三、HTTPS 的安全原理（核心考点）
HTTPS 并非直接对数据加密，而是通过 SSL/TLS 协议建立安全连接，流程如下：
握手阶段：客户端与服务器协商加密算法、交换公钥，并验证服务器 SSL 证书的有效性（确保连接的是真实服务器，防止中间人攻击）。
密钥交换：客户端用服务器公钥加密一个 “对称加密密钥”，发送给服务器（非对称加密，仅服务器私钥可解密）。
数据传输：后续所有数据均使用 “对称加密密钥” 加密（对称加密效率高，适合大量数据传输）。
四、为什么需要 HTTPS？
HTTP 的明文传输存在三大风险，而 HTTPS 可解决：
窃听风险：第三方可获取传输的敏感信息（如账号密码、支付数据）。
篡改风险：第三方可修改传输数据（如篡改商品价格、替换广告内容）。
伪造风险：第三方可伪装成服务器欺骗客户端（如钓鱼网站）。
HTTPS 通过加密和证书验证，确保数据的机密性（仅收发双方可见）、完整性（数据未被篡改）、真实性（确认服务器身份）。
面试总结
前端开发中，需注意：
现代浏览器对 HTTP 站点的限制（如标记 “不安全”、禁止加载 HTTPS 资源）。
调用接口时，若服务器用 HTTPS，前端资源（如 JS、图片）也需用 HTTPS（避免 “混合内容” 错误）。
核心区别一句话总结：HTTPS 是加了 SSL/TLS 加密层的 HTTP，更安全但性能稍低，需证书和 44 端口。
掌握这两点，能清晰回答协议本质和应用场景（如支付页面必须用 HTTPS，静态资源可酌情用 HTTP 优化速度）。`
              }
            ]
          },
          {
            question: `9.说说HTTP1.0/1.1/2.0的区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端开发中，HTTP 协议的版本差异直接影响网络请求性能和用户体验，是面试高频考点。以下从核心特性、性能优化等维度对比 HTTP 1.0、1.1、2.0 的区别：
一、HTTP 1.0（1996 年）
早期的 HTTP 版本，奠定了客户端 - 服务器请求响应模式的基础，但存在明显性能缺陷。
核心特点：
短连接：每次请求 / 响应完成后，TCP 连接立即关闭（Connection: close为默认值）。
缺陷：频繁创建 / 关闭连接会产生大量 TCP 握手 / 挥手开销，尤其对包含多个资源（如图片、CSS、JS）的页面，性能极差。
无持久化机制：每个资源请求需单独建立连接，无法复用连接。
头部信息不压缩：请求头和响应头以明文传输，无压缩机制，浪费带宽。
二、HTTP 1.1（1999 年）
对 1.0 的重大改进，解决了短连接的性能问题，成为长期主导的 HTTP 版本（至今仍广泛使用）。
核心改进（对比 1.0）：
长连接（持久连接）
默认启用Connection: keep-alive，TCP 连接在一次请求后不关闭，可复用给后续请求（同一域名下）。
缺陷：仍存在 “队头阻塞”—— 同一连接中，前一个请求未完成时，后续请求需排队等待，无法并行处理。
管道化（Pipelining）
允许客户端在收到前一个响应前，连续发送多个请求（无需等待每个响应返回）。
缺陷：实际应用中支持有限（浏览器兼容性差，且队头阻塞问题仍存在），多数浏览器默认禁用。
缓存机制优化
新增Cache-Control、ETag、If-Modified-Since等头部，更精细地控制缓存策略（如过期时间、验证新鲜度）。
其他增强
支持虚拟主机（Host请求头，实现一台服务器托管多个域名）。
支持断点续传（Range请求头，用于大文件分块下载）。
新增状态码（如 409 冲突、509 带宽限制）。
三、HTTP 2.0（2015 年）
基于 SPDY 协议发展而来，针对 1.1 的性能瓶颈（尤其是队头阻塞）进行了颠覆性优化，是现代 Web 性能提升的核心协议。
核心改进（对比 1.1）：
二进制分帧层（核心特性）
将请求 / 响应数据拆分为二进制 “帧”（Frame），每个帧有唯一标识（Stream ID），不同请求的帧可在同一 TCP 连接中乱序传输，最后由客户端按 ID 重组。
彻底解决 “队头阻塞” 问题，实现并行传输（同一连接中多个请求 / 响应可同时进行）。
多路复用
基于二进制分帧，同一 TCP 连接中可并发处理多个请求（无需像 1.1 那样限制同一域名的并发连接数，如浏览器通常限制 6-8 个）。
举例：一个页面的 10 个 JS/CSS 资源请求，在 2.0 中可通过 1 个 TCP 连接同时传输，而 1.1 需建立多个连接或排队。
头部压缩（HPACK 算法）
对重复的请求头 / 响应头（如User-Agent、Cookie）进行索引和压缩，减少头部传输体积（1.1 头部明文传输，冗余大）。
服务器推送（Server Push）
服务器可主动向客户端推送所需资源（如客户端请求index.html时，服务器可提前推送关联的style.css和app.js），无需等待客户端显式请求，减少请求次数。
优先级设置
客户端可给不同请求的帧设置优先级（如 CSS 优先于图片），服务器按优先级处理，优化资源加载顺序。
四、版本对比总结表
特性	HTTP 1.0	HTTP 1.1	HTTP 2.0
连接方式	短连接（默认关闭）	长连接（默认keep-alive）	长连接 + 多路复用
并发能力	无（每次请求新建连接）	有限（依赖多连接，存在队头阻塞）	高（二进制分帧，无队头阻塞）
头部处理	明文传输，无压缩	明文传输，无压缩	HPACK 压缩，减少冗余
服务器推送	不支持	不支持	支持
数据格式	文本	文本	二进制分帧
面试关键点
前端开发中，HTTP 2.0 的多路复用和服务器推送对性能优化影响最大（如减少连接开销、加速首屏加载）。
实际应用中，HTTP 2.0 通常需配合 HTTPS（主流浏览器仅支持 HTTPS 下的 2.0）。
记住核心差异：1.0 短连接→1.1 长连接→2.0 二进制分帧 + 多路复用，性能呈指数级提升。
掌握这些，能清晰说明不同版本的演进逻辑及对前端性能的影响。`
              }
            ]
          },
          {
            question: `10.为什么说HTTPS比HTTP安全?HTTPS是如何保证安全的?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，HTTPS 的安全性原理是高频考点，核心需理解其如何解决 HTTP 的安全缺陷。以下从 “HTTP 的安全问题” 和 “HTTPS 的保障机制” 两方面展开：
一、为什么说 HTTPS 比 HTTP 安全？
HTTP 协议存在三大安全缺陷，而 HTTPS 通过加密和身份验证解决了这些问题：
HTTP 数据明文传输：请求 / 响应内容（如密码、支付信息）以明文形式在网络中传输，黑客可通过 “中间人攻击” 直接窃取数据。
无身份验证机制：无法确认服务器是否为真实目标（可能被伪装的钓鱼网站欺骗），也无法确认客户端身份。
数据完整性无保障：传输过程中数据可能被篡改（如替换广告内容），且接收方无法察觉。
HTTPS 通过加密传输、身份验证和数据完整性校验，彻底解决了上述问题，因此更安全。
二、HTTPS 是如何保证安全的？
HTTPS 并非全新协议，而是 “HTTP + SSL/TLS” 的组合（SSL 是早期协议，TLS 是其升级版，现在通常统称为 TLS）。核心通过 TLS 层实现安全保障，关键机制包括：
1. 数据加密：混合加密算法（对称加密 + 非对称加密）
对称加密：加密和解密使用同一密钥（如 AES），速度快，但密钥传输过程中若被窃取，加密失效。
非对称加密：使用 “公钥 - 私钥” 对（如 RSA），公钥可公开，私钥仅服务器持有。特点：公钥加密的数据只能用私钥解密，私钥加密的数据只能用公钥解密，但加密速度慢。
HTTPS 的加密流程：
第一步（握手阶段）：通过非对称加密，客户端和服务器协商生成一个临时对称密钥（会话密钥）。
服务器将公钥发给客户端（包含在 SSL 证书中）。
客户端生成一个随机的 “预主密钥”，用服务器公钥加密后发送给服务器。
服务器用私钥解密，得到预主密钥，双方基于预主密钥生成相同的会话密钥。
第二步（传输阶段）：后续所有数据都用会话密钥（对称加密） 加密传输。
优势：兼顾安全性（非对称加密保障密钥安全）和效率（对称加密保障传输速度）。
2. 身份验证：SSL 证书（数字证书）
防止 “中间人伪装服务器”，通过第三方权威机构（CA，如 Let’s Encrypt、Verisign）颁发的 SSL 证书实现：
证书包含：服务器公钥、网站域名、证书有效期、CA 签名等信息。
验证过程：
客户端收到服务器的证书后，会向 CA 服务器验证证书的合法性（检查签名是否有效、域名是否匹配、是否过期）。
只有验证通过，客户端才会信任服务器并继续通信；若证书无效（如伪造、过期），浏览器会显示安全警告（如 “您的连接不是私密连接”）。
3. 数据完整性校验：消息摘要（哈希算法）
防止数据传输中被篡改，通过哈希算法（如 SHA-256）生成 “消息摘要”：
发送方：对原始数据计算哈希值（摘要），并将摘要与数据一起加密传输。
接收方：解密后对数据重新计算哈希值，与收到的摘要对比。若一致，说明数据未被篡改；否则，判定数据被修改，拒绝接收。
总结：HTTPS 的安全核心
加密：混合加密确保数据传输过程中无法被窃听。
认证：SSL 证书确保客户端连接的是真实服务器，防伪装。
校验：哈希算法确保数据未被篡改，保障完整性。
这三点共同构成了 HTTPS 的安全基础，也是其与 HTTP 的本质区别。在前端开发中，HTTPS 是现代网站的标配（尤其涉及用户隐私、支付等场景），了解其原理对理解网络安全和排查跨域、混合内容等问题至关重要。`
              }
            ]
          },
          {
            question: `11. 如何理解CDN？说说实现原理?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，CDN 是高频考点，尤其在涉及性能优化的场景中经常被提及。以下从概念、核心作用和实现原理三方面解析，帮助理解其本质：
一、什么是 CDN？
CDN（Content Delivery Network，内容分发网络）是一套分布式的网络加速服务。简单说，它通过在全球 / 全国部署大量 “边缘节点服务器”，将网站的静态资源（如图片、JS、CSS、视频等）缓存到这些节点上。当用户请求资源时，无需访问源服务器，而是从距离最近的边缘节点获取，从而提升访问速度。
二、CDN 的核心作用（为什么需要 CDN？）
加速资源加载：解决 “距离远导致的网络延迟” 问题（如北京用户访问美国的源服务器，延迟可能达数百毫秒，而访问国内边缘节点仅需几十毫秒）。
减轻源服务器压力：静态资源由边缘节点承担请求，减少源服务器的流量和负载。
提高可用性：边缘节点分布式部署，某一节点故障时，可自动切换到其他节点，避免单点故障。
抗网络攻击：通过节点分流，降低 DDoS 等攻击对源服务器的影响。
三、CDN 的实现原理（核心流程）
CDN 的工作流程可简化为 “用户请求 → 智能调度 → 边缘节点响应 → 源站同步” 四步，核心依赖两个关键技术：全局负载均衡（GSLB） 和缓存机制。
1. 全局负载均衡（GSLB）：决定 “从哪个节点获取资源”
当用户发起资源请求（如访问https://cdn.example.com/image.png）时，第一步是通过 GSLB 选择最优的边缘节点，规则包括：
地理位置优先：选择物理距离用户最近的节点（如上海用户优先分配上海节点）。
节点负载状态：避开负载过高（如带宽满了、请求太多）的节点，选择空闲节点。
网络质量：选择与用户网络运营商（如电信、联通）一致的节点，减少跨网延迟（如电信用户优先分配电信节点）。
实现方式：通过 DNS 解析完成调度。CDN 会将域名（如cdn.example.com）解析到 GSLB 服务器，再由 GSLB 返回最优节点的 IP 地址，引导用户请求到该节点。
2. 缓存机制：边缘节点如何存储资源？
边缘节点并非一开始就有所有资源，而是通过 “首次请求缓存 + 后续直接复用” 的逻辑工作：
首次请求：若边缘节点没有用户需要的资源（如image.png），节点会向源服务器发起请求，获取资源后，一方面返回给用户，另一方面将资源缓存到本地（并设置缓存有效期，由源服务器通过 HTTP 响应头Cache-Control等控制）。
后续请求：若其他用户再请求相同资源，且资源仍在缓存有效期内，边缘节点直接从本地返回资源，无需访问源服务器。
缓存更新：当源服务器的资源更新（如图片替换），可通过 “主动推送更新” 或 “设置缓存过期时间” 让边缘节点同步新资源，避免用户获取旧内容。
3. 源站与 CDN 的协作
网站需将静态资源的域名切换为 CDN 提供的域名（如static.example.com改为cdn.example.com），并配置资源同步规则（如哪些资源需要缓存、缓存多久）。
边缘节点会定期与源站 “对账”，确保缓存资源的有效性（如过期资源自动从源站重新拉取）。
总结
CDN 的核心逻辑是 “就近访问 + 缓存复用”：通过分布式节点缩短物理距离，通过缓存减少源站依赖，最终实现资源加载加速。对前端而言，常用的 CDN 服务有阿里云 CDN、腾讯云 CDN、Cloudflare 等，日常开发中通过引入 CDN 上的第三方库（如https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js）即可间接利用其加速能力。`
              }
            ]
          },
          {
            question: `2.DNS放议是任么?说说DNS完整的查询过程？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，DNS 协议是网络基础的高频考点，尤其涉及域名解析流程与前端资源加载的关联。以下从概念和查询过程两方面详细解析：
一、什么是 DNS 协议？
DNS（Domain Name System，域名系统）是一套将域名（如www.baidu.com）转换为 IP 地址（如180.101.50.242）的协议。
原因：互联网中设备通过 IP 地址通信（类似 “门牌号”），但人类更易记忆域名（如网址），DNS 相当于 “网络世界的通讯录”，解决 “域名→IP” 的映射问题。
作用：是互联网的基础设施，所有基于域名的网络请求（如浏览网页、发送邮件）都依赖 DNS 解析。
二、DNS 完整的查询过程
当用户在浏览器输入www.baidu.com并回车时，DNS 查询流程可分为 **“递归查询”和“迭代查询”** 两个阶段，涉及多个服务器协作，核心是 “从本地到根服务器，逐步定位域名对应的 IP”。
整体流程（7 步核心步骤）：
本地主机缓存查询
浏览器 / 操作系统会先检查本地缓存：
浏览器缓存（如 Chrome 会缓存近期解析的域名，有效期几分钟到几小时）；
操作系统缓存（如 Windows 的hosts文件或系统级 DNS 缓存）。
若缓存中有www.baidu.com对应的 IP，直接返回，流程结束（最快，耗时 0-10ms）。
本地 DNS 服务器查询（递归查询起点）
若本地无缓存，请求会发送到本地 DNS 服务器（通常由网络运营商提供，如电信、联通的 DNS 服务器，或用户手动设置的公共 DNS 如114.114.114.114）。
本地 DNS 服务器先查自己的缓存，若有则直接返回；若无，则进入迭代查询阶段（本地 DNS 主动向其他服务器发起查询）。
根域名服务器查询
本地 DNS 服务器向根域名服务器（全球共 13 组，负责管理顶级域名，如.com、.cn）发送查询请求：“www.baidu.com的 IP 是什么？”
根服务器无法直接返回 IP，但会告知：“.com域名由顶级域名服务器管理，我给你它们的地址”（返回.com顶级服务器的 IP）。
顶级域名服务器查询
本地 DNS 服务器向.com顶级域名服务器发送请求：“www.baidu.com的 IP 是什么？”
顶级服务器同样无法直接返回，会告知：“baidu.com由其权威域名服务器管理，我给你它们的地址”（返回baidu.com域名对应的权威服务器 IP）。
权威域名服务器查询
本地 DNS 服务器向baidu.com的权威域名服务器（由百度公司维护，记录www.baidu.com等子域名的 IP 映射）发送请求。
权威服务器查询自身记录后，返回www.baidu.com对应的 IP 地址（如180.101.50.242）。
本地 DNS 服务器缓存并返回结果
本地 DNS 服务器将获取到的 IP 地址缓存（设置有效期，由域名的 TTL 参数控制，通常几分钟到几小时），然后将 IP 返回给用户的浏览器 / 操作系统。
浏览器发起最终请求
浏览器拿到 IP 地址后，通过 HTTP/HTTPS 协议向该 IP 对应的服务器发起请求，加载网页内容。
三、关键补充（面试加分点）
DNS 缓存层级：从近到远依次为 “浏览器缓存→操作系统缓存→本地 DNS 服务器缓存→根 / 顶级 / 权威服务器”，缓存的目的是减少重复查询，提升解析速度。
DNS 优化：前端可通过 “DNS 预解析”（<link rel="dns-prefetch" href="https://cdn.example.com">）提前解析域名，减少后续请求的等待时间。
DNS 负载均衡：权威服务器可返回多个 IP（如www.baidu.com对应多个服务器 IP），实现请求分流，提升网站可用性。
简言之，DNS 查询的核心是 “逐级查询、缓存加速”，通过多服务器协作将域名转换为 IP，是前端资源加载的 “第一棒”。`
              }
            ]
          },
          {
            question: `13.说说对WebSocket的理解？应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，WebSocket 是高频考点，尤其涉及实时通信场景的技术选型。以下从概念、核心特性、与 HTTP 的区别及应用场景展开解析：
一、什么是 WebSocket？
WebSocket 是一种全双工通信协议（RFC 6455 标准），允许客户端（如浏览器）与服务器建立持久化的 TCP 连接，实现双方双向实时通信。
传统 HTTP 协议是 “请求 - 响应” 模式（客户端主动发起请求，服务器被动回复），无法实现服务器主动向客户端推送数据；
WebSocket 解决了这一问题：连接建立后，客户端和服务器可随时向对方发送数据，无需频繁创建连接，适合实时场景。
二、WebSocket 的核心特性
持久连接
一次握手建立连接后，TCP 连接保持打开状态，避免 HTTP 反复建立连接的开销（如三次握手、头部传输等）。
全双工通信
客户端和服务器可同时发送数据（双向平等），而 HTTP 是单向的（客户端请求→服务器响应）。
轻量协议
数据帧格式简单，头部开销小（相比 HTTP 的复杂头部），传输效率更高。
基于 TCP
底层依赖 TCP 协议保证可靠性，同时兼容 HTTP 的端口（80/443），可穿透大部分防火墙。
无同源限制
客户端可与任意域名的服务器建立 WebSocket 连接（需服务器配置允许跨域），比 AJAX 的同源策略更灵活。
三、WebSocket 与 HTTP 的区别
对比维度	HTTP	WebSocket
通信模式	单向（请求 - 响应）	双向（全双工）
连接方式	短连接（请求后关闭）	长连接（一次握手后持久保持）
服务器主动性	被动响应，无法主动推送	可主动向客户端推送数据
头部开销	每次请求携带完整头部（较大）	仅握手阶段用 HTTP 头部，后续轻量
适用场景	静态资源加载、非实时接口请求	实时通信（聊天、数据监控等）
四、WebSocket 的连接建立过程（握手阶段）
WebSocket 虽然独立于 HTTP，但握手阶段依赖 HTTP 协议，流程如下：
客户端发起 HTTP 请求，请求头中包含 WebSocket 标识：
http
GET /chat HTTP/1.1  
Host: example.com  
Upgrade: websocket  // 声明要升级为WebSocket协议  
Connection: Upgrade  // 配合Upgrade表示升级连接  
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==  // 客户端生成的随机密钥  
Sec-WebSocket-Version: 13  // 协议版本  

服务器确认升级，返回 101 状态码（切换协议），并通过Sec-WebSocket-Accept验证密钥：
http
HTTP/1.1 101 Switching Protocols  
Upgrade: websocket  
Connection: Upgrade  
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=  // 服务器对客户端密钥的加密结果  

握手成功后，HTTP 连接升级为 WebSocket 连接，双方通过二进制帧传输数据。
五、应用场景（面试重点）
WebSocket 的核心优势是 “实时双向通信”，适合以下场景：
实时聊天系统
如网页版微信、企业 IM（钉钉 / 飞书的网页端），需要即时收发消息，WebSocket 可避免轮询（如 AJAX 定时请求）的延迟和资源浪费。
实时数据监控
如股票行情、物联网设备状态（温度 / 湿度实时更新）、服务器性能监控（CPU / 内存使用率），服务器可主动推送最新数据到前端。
协作编辑工具
如在线文档（腾讯文档、飞书文档）、多人画板，用户操作（输入文字、绘图）需实时同步给其他协作者。
游戏场景
实时多人在线游戏（如网页版斗地主、IO 类游戏），玩家操作（移动、攻击）需即时同步到服务器和其他玩家客户端。
实时通知
如电商网站的订单状态更新（“已发货” 提醒）、社交平台的消息通知（点赞 / 评论提醒），服务器可主动推送通知给用户。
六、前端使用 WebSocket 的简单示例
javascript
// 建立连接（ws://非加密，wss://加密，类似http/https）  
const ws = new WebSocket('wss://example.com/chat');  

// 连接成功回调  
ws.onopen = () => {  
  console.log('连接已建立');  
  ws.send('Hello Server!'); // 向服务器发送数据  
};  

// 接收服务器消息  
ws.onmessage = (event) => {  
  console.log('收到消息:', event.data); // event.data为服务器发送的数据（字符串/二进制）  
};  

// 连接关闭回调  
ws.onclose = () => {  
  console.log('连接已关闭');  
};  

// 错误处理  
ws.onerror = (error) => {  
  console.error('错误:', error);  
};  





总结
WebSocket 是解决 “实时通信” 的关键技术，通过持久连接和双向通信，弥补了 HTTP 在实时场景的不足。面试中需重点掌握其与 HTTP 的区别、握手过程及典型应用场景，体现对实时交互需求的技术理解。`
              }
            ]
          },
          {
            question: `14.说说地址栏输入URL敲下回车后发生了什么？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `这是前端面试中的经典问题，考察对网络请求全流程、浏览器工作原理的综合理解。整个过程可拆解为以下10 个核心步骤，从 URL 解析到页面渲染逐步展开：
一、URL 解析与校验
判断输入内容
浏览器首先判断用户输入的是 “URL”（如https://www.example.com）还是 “搜索关键词”（如 “前端面试题”）。
若为关键词，会调用默认搜索引擎（如 Chrome 用 Google，百度浏览器用百度）拼接成搜索 URL（如https://www.baidu.com/s?wd=前端面试题）。
URL 结构解析
分解 URL 的各部分：协议（https）、域名（www.example.com）、端口（默认443，HTTP 默认80）、路径（/index.html）、查询参数（?id=1）、锚点（#top）等。
锚点（#后的内容）仅在客户端生效，不会发送到服务器。
二、缓存检查（减少请求开销）
浏览器会优先查询缓存，判断是否有可用的本地资源，避免重复请求：
检查强缓存（Cache-Control、Expires）
若资源未过期（如Cache-Control: max-age=3600表示 1 小时内有效），直接使用本地缓存，不发起网络请求。
强缓存失效则检查协商缓存（Last-Modified/If-Modified-Since、ETag/If-None-Match）
浏览器发送请求头携带资源标识，服务器对比后若资源未修改，返回304 Not Modified，浏览器使用本地缓存；若已修改，返回新资源（200 OK）。
三、DNS 域名解析（将域名转为 IP）
域名（如www.example.com）是便于记忆的标识，实际通信依赖 IP 地址，需通过 DNS 解析获取：
本地 DNS 缓存查询
先查浏览器缓存（如 Chrome 可通过chrome://net-internals/#dns查看）和操作系统缓存（如 Windows 的hosts文件），若有记录直接返回 IP。
递归查询与迭代查询
若本地无缓存，操作系统向本地 DNS 服务器（如路由器分配的 ISP DNS）发起请求。
本地 DNS 服务器采用迭代查询：依次向 “根域名服务器→顶级域名服务器（如.com）→权威域名服务器” 查询，最终返回目标 IP（如192.168.1.1）。
（补充：DNS 解析可通过CDN 的智能 DNS优化，返回离用户最近的节点 IP）。
四、建立 TCP 连接（三次握手）
通过 IP 地址找到目标服务器后，浏览器与服务器通过TCP 协议建立可靠连接（HTTPS 在此基础上多一步 TLS 握手）：
第一次握手：客户端发送SYN包（同步序列编号），表示 “请求建立连接”。
第二次握手：服务器收到SYN后，返回SYN+ACK包（同意连接 + 确认）。
第三次握手：客户端收到SYN+ACK后，返回ACK包（确认收到），连接正式建立。
目的：确保双方 “发送能力” 和 “接收能力” 均正常，避免无效通信。
五、HTTPS 额外：TLS 握手（加密通信）
若 URL 协议为HTTPS，在 TCP 连接后需完成TLS 加密握手（以 TLS 1.2 为例）：
客户端发送 “支持的加密套件、随机数Client Random”。
服务器返回 “选中的加密套件、随机数Server Random、服务器证书（含公钥）”。
客户端验证证书有效性（通过 CA 机构），生成 “预主密钥Pre-Master Secret”，用服务器公钥加密后发送给服务器。
双方基于Client Random、Server Random、Pre-Master Secret，通过相同算法生成会话密钥（对称加密密钥）。
客户端和服务器分别发送 “已准备好使用会话密钥加密通信” 的通知，握手完成。
六、发送 HTTP 请求
连接建立后，浏览器向服务器发送HTTP 请求报文，包含：
请求行：方法（GET/POST等）、URL 路径、协议版本（如HTTP/1.1）。
请求头：Host（目标域名）、User-Agent（浏览器信息）、Cookie、Accept（支持的资源类型）等。
请求体：POST等方法时携带的数据（如表单数据、JSON）。
七、服务器处理请求并返回响应
服务器（如 Nginx、Node.js）接收请求后：
解析请求路径、参数、Cookie 等信息。
业务处理（如查询数据库、调用接口）。
生成HTTP 响应报文，包含：
响应行：状态码（200 OK、404 Not Found等）、协议版本。
响应头：Content-Type（资源类型，如text/html）、Set-Cookie、Cache-Control（缓存策略）等。
响应体：实际资源（HTML、CSS、JS、图片等）。
八、关闭 TCP 连接（四次挥手，可选）
若为 HTTP/1.0（短连接）或 HTTP/1.1 且Connection: close，请求完成后关闭连接：
客户端发送FIN包（请求关闭）。
服务器返回ACK（确认关闭请求）。
服务器处理完剩余数据后，发送FIN包（准备关闭）。
客户端返回ACK（确认关闭），连接断开。
若为 HTTP/1.1 默认的 “长连接”（Connection: keep-alive），连接会保持一段时间，供后续请求复用。
九、浏览器解析与渲染页面
服务器返回的 HTML 等资源，需经浏览器解析→渲染成可视化页面，核心步骤：
HTML 解析：生成 DOM 树（文档对象模型）。
CSS 解析：生成 CSSOM 树（CSS 对象模型）。
合并 DOM+CSSOM：生成渲染树（只包含可见元素，排除display: none等）。
布局（Layout）：计算元素的位置、大小（回流 / 重排）。
绘制（Paint）：将元素绘制到图层（重绘）。
合成（Composite）：将多个图层合并为最终屏幕图像（如处理 z-index、动画）。
十、执行 JS 脚本
解析 HTML 时遇到<script>标签会暂停 HTML 解析（JS 可操作 DOM/CSSOM），优先执行 JS（可通过async/defer异步加载优化）。
JS 执行过程中可能触发 DOM/CSSOM 更新，导致重新布局、绘制或合成。
总结
整个流程可概括为：“URL 解析→缓存检查→DNS 解析→TCP 连接→（HTTPS 的 TLS 握手）→HTTP 请求→服务器响应→TCP 关闭→页面渲染”。
面试中需重点突出缓存机制、DNS 流程、TCP 三次握手、HTTPS 加密、浏览器渲染原理等细节，体现对全链路的理解。`
              }
            ]
          }
        ]
      },
      {
        tag: `React面试题`,
        data: [
          {
            question: `1.说说对React的理解?有哪些特性?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，“对 React 的理解及特性” 是高频基础题，考察对框架核心思想和设计理念的掌握。以下从定义、核心特性两方面展开，突出 React 的设计初衷和优势：
一、对 React 的理解
React 是由 Facebook（现 Meta）开发并开源的声明式 UI 库，专注于构建用户界面（UI），尤其擅长处理复杂交互的单页应用（SPA）。其核心设计理念可概括为：
“用组件化思想拆分 UI”：将页面拆分为独立、可复用的组件（如按钮、表单、卡片），通过组合组件构建完整页面，提高代码复用性和维护性。
“数据驱动视图”：通过状态（State）管理数据，当数据变化时，React 会自动更新对应的 UI 部分，开发者无需手动操作 DOM，只需关注数据逻辑。
“声明式编程”：开发者只需描述 “UI 应该是什么样子”（如 “当数据为 true 时显示按钮”），而非 “如何一步步操作 DOM 实现”（如原生 JS 的appendChild），简化了复杂交互的实现。
二、React 的核心特性
声明式编程
与命令式编程（需手动描述 DOM 操作步骤）不同，React 允许开发者用类似 “描述结果” 的方式编写代码。
例：当用户点击按钮切换显示状态时，只需修改isShow状态，React 会自动处理 DOM 的显示 / 隐藏，无需调用style.display。
组件化
组件是 React 的基本单位，可分为函数组件（简洁，推荐）和类组件（包含生命周期等特性，逐渐被 Hooks 替代）。
组件具有封装性（内部逻辑私有）、复用性（可在多个地方调用）、组合性（组件可嵌套其他组件，形成组件树）。
例：一个Header组件可包含Logo、Nav子组件，通过 props 传递数据实现通信。
虚拟 DOM（Virtual DOM）
React 会在内存中维护一份与真实 DOM 结构一致的虚拟 DOM（JavaScript 对象）。
当数据变化时，React 先对比新旧虚拟 DOM 的差异（Diffing 算法），只将变化的部分更新到真实 DOM（DOM Diff），减少直接操作 DOM 的性能开销（真实 DOM 操作是前端性能瓶颈之一）。
单向数据流
数据在组件树中沿单一方向传递：父组件通过props向子组件传递数据，子组件不能直接修改props，需通过调用父组件传递的回调函数修改父组件的状态，再由父组件重新传递数据。
优势：数据流向清晰，便于追踪状态变化，降低调试难度（避免双向绑定可能导致的 “数据混乱”）。
Hooks（React 16.8 + 新增）
解决类组件的痛点（如生命周期逻辑分散、复用状态逻辑复杂），允许在函数组件中使用状态和其他 React 特性（如useState管理状态、useEffect处理副作用）。
常用 Hooks：useState（状态管理）、useEffect（处理副作用，如 API 请求、事件监听）、useContext（跨组件传值）、useReducer（复杂状态逻辑）等。
JSX 语法
JSX 是 React 的语法扩展（JavaScript XML），允许在 JavaScript 中直接编写 HTML-like 代码，简化 UI 描述。
例：const element = <h1>Hello, React</h1>，本质上会被 Babel 编译为React.createElement函数调用，最终生成虚拟 DOM 对象。
注意：JSX 不是 HTML，有自己的语法规则（如className替代class、htmlFor替代for）。
跨平台能力
基于 React 的核心思想，衍生出跨平台解决方案：
React Native：用 React 语法开发原生移动应用（iOS/Android），实现 “一次编写，多端运行”。
React 360：开发 VR 应用，拓展了 React 的应用场景。
总结
React 的核心是 **“组件化” 和 “数据驱动”**，通过虚拟 DOM 和声明式编程提高开发效率和性能，Hooks 的出现进一步增强了函数组件的能力。其设计理念让开发者能更专注于业务逻辑，而非 DOM 操作，非常适合构建复杂、交互频繁的现代 Web 应用。面试中可结合实际开发体验，举例说明特性的应用（如用组件化拆分页面、用 Hooks 管理状态），体现对框架的深入理解。`
              }
            ]
          },
          {
            question: `2. state 和 props 有什么区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在前端面试中，state和props是 React 的核心概念，考察对组件数据流动和状态管理的理解。以下从定义、特性、使用场景三方面对比解析：
一、核心定义
state（状态）
组件内部的私有数据存储，用于记录组件自身的动态信息（如表单输入值、加载状态、计数器等）。
只能在组件内部修改（通过this.setState或useState），不可被外部直接访问或修改。
props（属性）
从父组件传递给子组件的数据，用于组件间通信。
子组件只能读取props，不能直接修改（React 强制要求props是不可变的，类似函数参数）。
二、关键区别对比
对比维度	state	props
数据来源	组件内部定义和管理	由父组件传递而来
数据流向	单向（组件内部自管理）	单向（父→子，不可逆流）
可变性	可变（通过setState/useState修改）	不可变（子组件禁止修改 props）
用途	存储组件内部动态数据（如用户输入、加载状态）	传递数据 / 回调函数，实现组件间通信
初始化位置	类组件：constructor或实例属性
函数组件：useState	父组件中通过 JSX 属性传递（如<Child name="张三" />）
组件关系	每个组件独立维护自己的 state	子组件接收父组件的 props
三、典型使用场景
state的场景
表单输入（如用户输入的用户名、密码）：
javascript
const [username, setUsername] = useState('');  
<input onChange={(e) => setUsername(e.target.value)} />  

条件渲染（如控制弹窗显示 / 隐藏）：
javascript
const [isModalOpen, setIsModalOpen] = useState(false);  
<button onClick={() => setIsModalOpen(true)}>打开弹窗</button>  

列表数据（如动态添加的待办事项）：
javascript
const [todos, setTodos] = useState([]);  
<button onClick={() => setTodos([...todos, { id: Date.now(), text: '新任务' }])}>添加</button>  

props的场景
父组件向子组件传递数据：
javascript
// 父组件  
<UserCard name="张三" age={25} />  

// 子组件  
const UserCard = ({ name, age }) => <div>{name}，{age}岁</div>;  

子组件向父组件通信（通过回调函数）：
javascript
// 父组件  
const handleDelete = (id) => { /* 删除逻辑 */ };  
<TodoItem onDelete={handleDelete} />  

// 子组件  
const TodoItem = ({ onDelete }) => <button onClick={() => onDelete(id)}>删除</button>;  

四、常见误区与面试陷阱
误区：props只能传数据，不能传函数
正解：props可传递任何类型的值，包括函数。通过传递回调函数，子组件可触发父组件的逻辑（如上述 “子组件向父组件通信”）。
误区：state必须在构造函数中初始化
正解：在类组件中，state可在构造函数中初始化（this.state = { count: 0 }），也可直接作为实例属性（state = { count: 0 }）；函数组件用useState初始化。
面试陷阱：如何让子组件修改父组件数据？
正确思路：子组件不能直接修改父组件的state，但可通过父组件传递的回调函数间接修改。例如：
javascript
// 父组件  
const [count, setCount] = useState(0);  
const increment = () => setCount(count + 1);  
<Child onIncrement={increment} />  

// 子组件  
const Child = ({ onIncrement }) => <button onClick={onIncrement}>增加</button>;  

总结
state是组件内部的 “私有状态”，用于存储动态变化的数据，通过setState/useState修改。
props是组件间的 “通信桥梁”，用于父→子传递数据，子组件不可修改。
数据流向永远是单向的（父→子），若需子→父通信，需通过回调函数实现。
掌握这两点，能清晰应对 React 组件设计和数据流相关问题（如多层级组件通信、状态提升等）。`
              }
            ]
          },
          {
            question: `3. super()和 super(props)有什么区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 的类组件中，super()和super(props)的区别涉及 JavaScript 继承机制和 React 的 props 传递逻辑，是面试中的高频考点。以下从语法、作用、使用场景三方面解析：
一、核心区别
在类组件的构造函数中，super()是调用父类（React.Component）构造函数的方法，其传参与否的区别如下：
写法	作用	使用场景
super()	调用父类构造函数，不传递 props。子类可通过this.props访问 props（React 自动赋值）。	无需在构造函数中使用 props 时。
super(props)	调用父类构造函数，并将 props 显式传递给父类。子类需通过props参数访问 props（而非this.props）。	需要在构造函数中使用 props 时（如初始化 state）。
二、深入解析
super()的工作原理
在子类构造函数中，必须先调用super()才能使用this（JavaScript 强制要求）。
super()调用后，React 会自动将 props 赋值给this.props，因此后续可直接通过this.props访问。
javascript
class MyComponent extends React.Component {  
  constructor(props) {  
    super(); // 不传递props  
    console.log(this.props); // ✅ 可访问，React自动赋值  
  }  
}  

super(props)的特殊场景
若需在构造函数中使用 props 初始化 state或绑定方法，需显式传递props给super()。
此时，父类构造函数会将 props 作为参数接收，但 React 仍会自动将 props 赋值给this.props，因此后续仍可通过this.props访问。
javascript
class MyComponent extends React.Component {  
  constructor(props) {  
    super(props); // 显式传递props  
    this.state = {  
      initialValue: props.value // ✅ 可通过props参数访问  
    };  
  }  
}  

误区澄清
super(props)不会影响this.props的赋值：无论是否传递 props，React 都会自动将 props 赋值给this.props。
传递 props 的真正作用：是让父类构造函数中的props参数可用（否则父类构造函数中的props为undefined）。
三、示例对比
场景 1：无需在构造函数中使用 props
javascript
class MyComponent extends React.Component {  
  constructor(props) {  
    super(); // 推荐写法  
    // 无需使用props，直接初始化state  
    this.state = { count: 0 };  
  }  
  render() {  
    return <div>{this.props.message}</div>; // ✅ 可正常访问props  
  }  
}  
场景 2：需在构造函数中使用 props
javascript
class MyComponent extends React.Component {  
  constructor(props) {  
    super(props); // 必须传递props，否则props参数为undefined  
    // 用props初始化state  
    this.state = { initialValue: props.value };  
    // 绑定方法（需使用props中的回调函数）  
    this.handleClick = this.handleClick.bind(this, props.callback);  
  }  
}  
四、函数组件（Hooks）的等价写法
在函数组件中，无需考虑super()，直接通过参数接收 props：
javascript
const MyComponent = (props) => {  
  const [count, setCount] = useState(props.initialCount); // 直接使用props  
  return <div>{props.message}</div>;  
};  
五、面试总结
super()与super(props)的核心区别
super()：适用于无需在构造函数中使用 props 的场景，React 会自动赋值this.props。
super(props)：适用于需在构造函数中使用 props 的场景（如初始化 state、绑定方法），确保父类构造函数能接收到 props。
常见误区
认为super(props)是为了让this.props可用（错误）—— 无论是否传递 props，this.props都可用。
混淆 JavaScript 继承机制与 React 的 props 处理逻辑。
最佳实践
若需在构造函数中使用 props，显式传递super(props)。
若无需使用 props，直接用super()（更简洁）。
优先使用函数组件（避免类组件的super()问题）。
掌握这些细节，能在面试中清晰区分两者的使用场景，体现对 React 和 JavaScript 继承的深入理解。`
              }
            ]
          },
          {
            question: `4.说说对React中类组件和函数组件的理解？有什么区别？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，类组件和函数组件是创建组件的两种方式，它们在语法、状态管理和生命周期等方面存在明显差异。以下是详细对比：
1. 语法差异
类组件：继承自React.Component，使用class关键字定义，需要定义render()方法返回 JSX。
函数组件：使用普通函数或箭头函数定义，直接返回 JSX（也称为无状态组件或纯组件）。
jsx
// 类组件
class ClassComponent extends React.Component {
  render() {
    return <div>Hello, {this.props.name}</div>;
  }
}

// 函数组件
const FunctionComponent = (props) => {
  return <div>Hello, {props.name}</div>;
};
2. 状态管理
类组件：使用this.state定义和管理本地状态，通过this.setState()更新。
函数组件：本身无状态，需使用 Hooks（如useState）管理状态。
jsx
// 类组件的状态
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// 函数组件的状态（使用useState Hook）
const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
3. 生命周期方法
类组件：有完整的生命周期方法（如componentDidMount、componentDidUpdate、componentWillUnmount）。
函数组件：没有生命周期方法，需使用useEffect Hook 模拟。
jsx
// 类组件的生命周期
class Example extends React.Component {
  componentDidMount() {
    console.log('Mounted');
  }
  componentDidUpdate() {
    console.log('Updated');
  }
  componentWillUnmount() {
    console.log('Unmounting');
  }
  render() { ... }
}

// 函数组件使用useEffect模拟生命周期
const Example = () => {
  useEffect(() => {
    console.log('Mounted/Updated'); // 相当于componentDidMount + componentDidUpdate
    return () => console.log('Unmounting'); // 相当于componentWillUnmount
  }, []); // 依赖项为空数组时，仅在挂载和卸载时执行
  return ...;
};
4. 性能与优化
类组件：需通过shouldComponentUpdate或继承React.PureComponent进行浅比较优化。
函数组件：使用React.memo进行浅比较，避免不必要的重渲染。
jsx
// 类组件优化
class MyComponent extends React.PureComponent { ... }

// 函数组件优化
const MyComponent = React.memo((props) => { ... });
5. 上下文与 refs
类组件：通过this.context访问上下文，通过React.createRef()或ref属性使用 refs。
函数组件：使用useContext Hook 访问上下文，使用useRef Hook 创建 refs。
jsx
// 函数组件使用useContext和useRef
const MyComponent = () => {
  const theme = useContext(ThemeContext);
  const inputRef = useRef(null);
  return <input ref={inputRef} />;
};
6. 适用场景
类组件：需要复杂状态管理、生命周期控制或内部状态逻辑的场景。
函数组件：UI 展示组件、无状态逻辑的纯渲染组件，或配合 Hooks 简化代码。
核心区别总结
特性	类组件	函数组件
语法	基于class	基于函数 / Hooks
状态管理	this.state + setState	useState/useReducer
生命周期	有完整生命周期方法	依赖useEffect模拟
优化方式	PureComponent或shouldComponentUpdate	React.memo
上下文 /refs	this.context/createRef	useContext/useRef
代码复杂度	较高（状态逻辑、生命周期）	较低（Hooks 简化逻辑）
为什么函数组件更推荐？
代码简洁：无冗余的类语法，逻辑更清晰。
状态管理简化：Hooks 使状态逻辑可复用（如useState、useReducer）。
避免this指向问题：类组件中this的绑定问题是常见痛点。
更好的性能优化：React.memo默认浅比较，减少不必要渲染。
React 官方趋势：未来新特性将更侧重函数组件与 Hooks。
总结：类组件适合复杂状态管理和生命周期操作，而函数组件是 React 未来的主流方向，推荐优先使用。`
              }
            ]
          },
          {
            question: `5.说说对受控组件和非受控组件的理解？应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，受控组件和非受控组件是处理表单数据的两种不同方式，它们的核心区别在于数据控制权的归属。以下是详细对比和应用场景分析：
1. 基本概念
受控组件
数据由 React 组件控制：表单元素（如input、select）的值通过value属性绑定到组件状态，用户输入触发事件更新状态，形成单向数据流。
必须配合事件处理函数（如onChange）来更新状态，否则输入框只读。
jsx
// 受控组件示例
const ControlledInput = () => {
  const [value, setValue] = useState('');
  const handleChange = (e) => setValue(e.target.value);
  
  return <input value={value} onChange={handleChange} />;
};
非受控组件
数据由 DOM 本身控制：表单元素的值直接存储在 DOM 中，通过ref获取真实 DOM 节点的值，无需状态管理。
** 通常使用defaultValue** 初始化值（仅在挂载时生效，后续更新由用户操作决定）。
jsx
// 非受控组件示例
const UncontrolledInput = () => {
  const inputRef = useRef(null);
  
  const handleSubmit = () => {
    console.log(inputRef.current.value); // 直接访问DOM值
  };
  
  return <input ref={inputRef} defaultValue="初始值" />;
};
2. 核心区别
特性	受控组件	非受控组件
数据存储位置	组件状态（如useState）	DOM 节点（通过ref访问）
数据流向	单向数据流：状态 → DOM → 事件 → 状态	双向数据流：用户直接操作 DOM
更新方式	必须通过事件处理函数更新状态	无需事件处理，用户输入直接反映到 DOM
初始化	使用value属性绑定初始值	使用defaultValue或defaultChecked
实时验证	支持（状态更新时触发验证逻辑）	不支持（需手动监听事件）
适用场景	复杂表单逻辑、实时数据处理、双向绑定	简单表单（如文件上传、一次性提交）
3. 应用场景
受控组件适用场景
实时数据处理：如搜索框联想、表单实时验证（输入时校验格式）。
双向数据绑定：需要状态与表单值同步的场景（如数据回显、撤销操作）。
复杂表单逻辑：如多字段联动（选择省份后动态加载城市列表）。
强制输入格式：如电话号码格式化（输入时自动添加分隔符）。
jsx
// 实时验证示例
const EmailInput = () => {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  
  const validateEmail = (value) => {
    const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    setError(isValid ? '' : '无效邮箱格式');
  };
  
  return (
    <div>
      <input 
        type="email" 
        value={email} 
        onChange={(e) => {
          setEmail(e.target.value);
          validateEmail(e.target.value);
        }} 
      />
      {error && <span style={{ color: 'red' }}>{error}</span>}
    </div>
  );
};
非受控组件适用场景
简单表单提交：如登录表单、一次性提交的表单（无需实时反馈）。
文件上传：input[type="file"]必须使用非受控（React 无法控制文件选择）。
集成第三方库：如富文本编辑器（内容由外部库管理）。
性能优化：减少状态更新频率（大量表单字段时）。
jsx
// 文件上传示例
const FileUpload = () => {
  const fileInputRef = useRef(null);
  
  const handleSubmit = () => {
    const file = fileInputRef.current.files[0];
    if (file) {
      console.log('上传文件:', file.name);
      // 处理文件上传逻辑
    }
  };
  
  return (
    <div>
      <input type="file" ref={fileInputRef} />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
};
4. 优缺点对比
受控组件
优点：
数据流向清晰，便于调试和测试。
支持实时验证和复杂交互逻辑。
状态可预测，符合 React 单向数据流理念。
缺点：
代码复杂度较高（需维护状态和事件处理）。
大量表单字段时可能影响性能（频繁状态更新）。
非受控组件
优点：
代码简洁，无需状态管理。
直接操作 DOM，适合简单场景。
性能略优（减少状态更新）。
缺点：
数据与 DOM 耦合，不利于复杂逻辑扩展。
缺乏状态记录，难以实现撤销、回显等功能。
5. 如何选择？
优先使用受控组件：大多数场景下，受控组件提供更可控的数据流和更好的可维护性。
考虑非受控组件：当表单逻辑简单、无需实时数据处理或需直接操作 DOM 时（如文件上传）。
混合使用：复杂表单中可部分字段用受控组件（需实时验证），部分用非受控组件（如文件上传）。
常见面试题扩展
如何实现受控组件的默认值？
使用useState的初始值：const [value, setValue] = useState('默认值');
非受控组件如何获取最新值？
通过ref.current.value获取 DOM 节点的当前值。
受控组件和非受控组件在性能上有何差异？
受控组件因频繁状态更新可能略慢，非受控组件直接操作 DOM 更高效。
React 官方推荐哪种方式？
官方推荐优先使用受控组件，因其更符合 React 的数据流动原则。
总结：受控组件适合复杂交互和数据驱动场景，非受控组件适合简单表单和直接 DOM 操作。理解两者差异并根据场景选择是关键。`
              }
            ]
          },
          {
            question: `6. 说说React的事件机制?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React 的事件机制是其核心特性之一，它在原生 DOM 事件基础上进行了封装和抽象，实现了跨浏览器兼容、合成事件系统和事件委托等特性。以下是详细解析：
1. 基本概念
合成事件（SyntheticEvent）
跨浏览器兼容：React 封装了原生 DOM 事件，提供统一的 API（如e.preventDefault()、e.stopPropagation()），屏蔽了浏览器差异。
对象池复用：合成事件对象会被复用，事件处理函数执行后，属性会被清空（若需异步访问，需调用e.persist()）。
jsx
const Button = () => {
  const handleClick = (e) => {
    e.preventDefault(); // 阻止默认行为（跨浏览器兼容）
    console.log('Clicked:', e.target);
  };
  
  return <button onClick={handleClick}>Click me</button>;
};
事件委托（Event Delegation）
所有事件绑定到根 DOM：React 将所有事件监听器挂载到文档根节点（document），而非具体元素。
事件冒泡到根节点处理：根据事件源（e.target）找到对应的 React 组件，执行相应回调。
html
预览
<!-- React内部实现简化示意 -->
document.addEventListener('click', (nativeEvent) => {
  // 根据nativeEvent.target找到对应的React组件
  // 执行组件中定义的onClick回调
});
2. 事件绑定方式
JSX 中绑定
最常见方式，使用驼峰命名（如onClick、onChange）。
需注意箭头函数或bind可能导致的性能问题（可通过类属性或useCallback优化）。
jsx
// 箭头函数（每次渲染生成新函数）
<button onClick={() => handleClick(id)}>Click</button>

// 类方法绑定（推荐）
class MyComponent extends React.Component {
  handleClick = () => { ... }
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
DOM 节点上的原生事件
通过ref直接操作 DOM，绑定原生事件（不推荐，违背 React 数据驱动理念）。
jsx
const MyComponent = () => {
  const divRef = useRef(null);
  
  useEffect(() => {
    const div = divRef.current;
    div.addEventListener('click', () => console.log('原生点击'));
    return () => div.removeEventListener('click', ...);
  }, []);
  
  return <div ref={divRef}>原生事件示例</div>;
};
3. 事件传播与阻止
事件流阶段
捕获阶段：从文档根到目标元素（默认不触发，需显式指定capture）。
目标阶段：事件到达目标元素。
冒泡阶段：从目标元素返回文档根（默认触发）。
jsx
// 捕获阶段绑定（添加Capture后缀）
<div onClickCapture={() => console.log('捕获阶段')}>
  <button onClick={() => console.log('冒泡阶段')}>Click</button>
</div>
阻止传播
e.stopPropagation()：阻止事件冒泡到父组件。
e.nativeEvent.stopImmediatePropagation()：完全阻止后续所有事件（包括捕获和冒泡）。
4. 原生事件与合成事件混合使用
合成事件绑定到 React 组件，原生事件绑定到真实 DOM。
合成事件不会冒泡到原生事件：因为 React 事件委托在根节点处理，与原生事件系统分离。
jsx
class MixedEvents extends React.Component {
  componentDidMount() {
    // 原生事件（捕获阶段）
    document.addEventListener('click', () => console.log('原生捕获'));
  }
  
  render() {
    return (
      <div onClick={() => console.log('合成事件')}>
        Click me
      </div>
    );
  }
}
5. 事件优先级与执行顺序
原生捕获阶段事件（如document.addEventListener('click', ..., true)）。
React 合成捕获事件（如onClickCapture）。
React 合成冒泡事件（如onClick）。
原生冒泡阶段事件（如document.addEventListener('click', ...)）。
6. 常见面试题扩展
React 事件与原生事件的区别？
React 事件使用驼峰命名（如onClick），原生使用小写（如onclick）。
React 事件是合成事件，原生是 DOM 事件。
React 事件绑定方式不同（JSX vs addEventListener）。
React 事件委托到文档根，原生事件绑定到具体元素。
为什么 React 需要合成事件？
跨浏览器兼容：屏蔽不同浏览器事件系统的差异。
事件委托：减少内存占用（所有事件绑定到根节点）。
抽象层：便于实现 React 的特性（如时间分片、批量更新）。
如何在 React 中使用原生事件？
通过ref获取 DOM 节点，手动调用addEventListener和removeEventListener。
注意内存泄漏（需在组件卸载时移除监听器）。
React 事件中的e.persist()有什么作用？
默认情况下，合成事件对象会被复用，事件处理函数执行后属性会被清空。
调用e.persist()会从对象池中移除事件对象，允许异步访问。
总结
React 的事件机制通过合成事件和事件委托实现了高效、统一的事件处理：
优点：跨浏览器兼容、事件委托优化性能、抽象层便于框架扩展。
注意点：事件对象复用、合成事件与原生事件的执行顺序、性能优化（避免重复创建回调函数）。
理解 React 事件机制的底层原理，有助于编写更高效、更少 bug 的 React 应用。`
              }
            ]
          },
          {
            question: `7.React事件绑定的方式有哪些?区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，事件绑定是实现交互的基础，但不同的绑定方式在语法、性能和上下文处理上存在差异。以下是常见的事件绑定方式及其对比：
1. 类组件中的事件绑定
方式一：在 render 中使用箭头函数
jsx
class MyComponent extends React.Component {
  handleClick() {
    console.log('Clicked', this); // this指向组件实例
  }

  render() {
    return <button onClick={() => this.handleClick()}>Click</button>;
  }
}
优点：简洁，自动绑定this。
缺点：每次渲染都会创建新函数，可能导致子组件不必要的重渲染（除非用React.memo包裹）。
适用场景：需要传递参数的场景（如onClick={() => this.handleClick(id)}）。
方式二：使用 bind 在 render 中绑定
jsx
class MyComponent extends React.Component {
  handleClick() {
    console.log('Clicked', this);
  }

  render() {
    return <button onClick={this.handleClick.bind(this)}>Click</button>;
  }
}
优点：显式绑定this。
缺点：每次渲染都会创建新函数（性能问题同上）。
方式三：在构造函数中绑定
jsx
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // 只绑定一次
  }

  handleClick() {
    console.log('Clicked', this);
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
优点：只创建一个函数实例，避免重复创建。
缺点：代码冗余（每个方法都要在构造函数中绑定）。
方式四：使用类属性语法（箭头函数）
jsx
class MyComponent extends React.Component {
  handleClick = () => {
    console.log('Clicked', this); // 自动绑定this
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
优点：简洁，自动绑定this，只创建一个函数实例。
缺点：需要 Babel 插件支持（如@babel/plugin-proposal-class-properties）。
2. 函数组件中的事件绑定
方式一：直接使用箭头函数
jsx
const MyComponent = () => {
  const handleClick = () => {
    console.log('Clicked');
  };

  return <button onClick={handleClick}>Click</button>;
};
优点：简洁，函数组件无this问题。
缺点：每次渲染都会重新创建函数（多数场景无影响，除非是频繁渲染的组件）。
方式二：使用 useCallback 优化
jsx
const MyComponent = () => {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []); // 依赖项为空，只创建一次函数

  return <button onClick={handleClick}>Click</button>;
};
优点：通过依赖项控制函数创建，避免不必要的重渲染。
适用场景：将回调函数传递给子组件时（如React.memo包裹的子组件）。
3. 事件绑定方式对比
绑定方式	this 指向	是否重复创建函数	性能影响	代码复杂度
render 中箭头函数	自动绑定	是	高	低
render 中 bind	手动绑定	是	高	中
构造函数中 bind	手动绑定	否	低	中
类属性箭头函数	自动绑定	否	低	低
函数组件箭头函数	无需 this	是	低（多数场景）	低
函数组件 useCallback	无需 this	否	低	中
4. 常见面试题扩展
为什么需要绑定 this？
在 JavaScript 中，函数的this指向取决于调用方式。React 类组件的方法默认不绑定this，若不绑定，this会指向undefined（严格模式下）。
哪种绑定方式性能最优？
构造函数中 bind 和类属性箭头函数（只创建一次函数）。
函数组件中使用useCallback（需正确设置依赖项）。
useCallback 的作用是什么？
缓存回调函数，避免因引用变化导致子组件重渲染。例如：
jsx
const Child = React.memo(({ onClick }) => <button onClick={onClick}>Click</button>);

const Parent = () => {
  const handleClick = useCallback(() => {}, []); // 确保引用不变
  return <Child onClick={handleClick} />;
};

箭头函数和普通函数在事件绑定中的区别？
箭头函数：不绑定this，继承自父级作用域，适合需要保留上下文的场景。
普通函数：this指向调用者，需显式绑定（如bind）。
5. 最佳实践建议
优先使用类属性箭头函数（类组件）或普通箭头函数（函数组件）。
传递回调给子组件时，使用useCallback避免不必要的重渲染。
避免在 render 中使用箭头函数或 bind（除非需要传递参数）。
注意闭包陷阱：箭头函数会捕获定义时的变量值，可能导致数据过时（可通过useRef或useState的函数式更新解决）。
总结
React 事件绑定的核心是解决this指向和性能优化问题：
类组件：推荐使用类属性箭头函数（简洁且高效）。
函数组件：默认使用箭头函数，性能敏感场景使用useCallback。
性能优化：减少不必要的函数创建，合理使用React.memo和useCallback。`
              }
            ]
          },
          {
            question: `8.React构建组件的方式有哪些?区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，构建组件的方式随着版本迭代不断演进，主要分为类组件、函数组件和Hooks三种范式。以下是详细对比和应用场景分析：
1. 类组件（Class Components）
基于 ES6 class：继承React.Component或React.PureComponent。
完整的生命周期：componentDidMount、render、componentWillUnmount等。
内部状态管理：通过this.state和this.setState。
jsx
// 类组件示例
class Counter extends React.Component {
  state = { count: 0 };
  
  componentDidMount() {
    console.log('Mounted');
  }
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}
2. 函数组件（Function Components）
无状态纯函数：仅接收 props 并返回 JSX，无生命周期和内部状态（早期）。
语法简洁：避免类的复杂性，适合 UI 展示组件。
jsx
// 函数组件示例（早期无状态）
const Greeting = (props) => {
  return <div>Hello, {props.name}</div>;
};
3. 函数组件 + Hooks（自 React 16.8）
状态与副作用管理：通过useState、useEffect等 Hooks 替代类组件功能。
逻辑复用：自定义 Hooks 解决代码复用问题（如表单验证、API 请求）。
jsx
// 函数组件 + Hooks示例
const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Mounted/Updated');
    return () => console.log('Unmounting');
  }, []);
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
4. 核心区别对比
特性	类组件	函数组件（无 Hooks）	函数组件 + Hooks
状态管理	this.state + setState	无状态	useState/useReducer
生命周期	完整生命周期方法	无	useEffect模拟
代码复杂度	高（类语法、this 绑定）	低（纯函数）	中（需理解 Hooks 规则）
逻辑复用	高阶组件（HOC）或 Render Props	无法复用状态逻辑	自定义 Hooks（推荐）
性能优化	PureComponent或shouldComponentUpdate	无	React.memo + useCallback
上下文访问	this.context	无	useContext
Refs 使用	React.createRef()	无	useRef
适用场景	复杂状态逻辑、生命周期依赖	纯展示组件	所有场景（推荐）
5. 优缺点与适用场景
类组件
优点：
完整生命周期控制，适合复杂副作用管理（如定时器、订阅）。
清晰的状态管理（this.state）。
缺点：
代码冗余（类语法、构造函数、this 绑定）。
逻辑复用困难（HOC 和 Render Props 导致层级嵌套）。
适用场景：
遗留项目维护。
需要细粒度生命周期控制的场景（如优化滚动性能）。
函数组件（无 Hooks）
优点：
代码简洁，易于理解和测试。
无状态，纯函数特性利于性能优化（React.memo）。
缺点：
无法管理状态和副作用，功能受限。
适用场景：
纯展示组件（如 Button、Card）。
早期 React 项目中的简单组件。
函数组件 + Hooks
优点：
状态逻辑复用（自定义 Hooks），避免代码冗余。
语法简洁，减少类组件的复杂性。
副作用统一管理（useEffect替代多个生命周期方法）。
缺点：
学习曲线较陡（需理解闭包陷阱、依赖项规则）。
适用场景：
所有场景（官方推荐）。
复杂状态逻辑（如表单处理、异步请求）。
6. 常见面试题扩展
为什么 React 官方推荐使用函数组件？
代码更简洁，符合 React 的函数式编程理念。
Hooks 解决了类组件的痛点（如逻辑复用、this 绑定）。
未来新特性将优先支持函数组件。
类组件和函数组件在性能上有何差异？
类组件需通过shouldComponentUpdate或PureComponent手动优化。
函数组件可通过React.memo和useCallback自动优化，避免不必要渲染。
如何在函数组件中模拟类组件的生命周期？
componentDidMount：useEffect(() => {}, [])（依赖项为空）。
componentDidUpdate：useEffect(() => { /* 初始化不执行 */ }, [dep])（有依赖项）。
componentWillUnmount：useEffect(() => () => { /* 清理函数 */ }, [])。
Hooks 相比高阶组件（HOC）和 Render Props 有什么优势？
HOC 和 Render Props：通过组件嵌套共享逻辑，导致 “嵌套地狱”（Wrapper Hell）。
Hooks：通过函数组合共享逻辑，不增加组件层级，代码更扁平。
总结
React 组件构建方式的演进体现了框架对代码简洁性和逻辑复用性的追求：
类组件：适合复杂生命周期管理，但逐渐被 Hooks 替代。
函数组件 + Hooks：React 未来的主流方向，推荐优先使用。
选择原则：除非必须使用生命周期方法，否则应使用函数组件 + Hooks。`
              }
            ]
          },
          {
            question: `9.说说react中引入css的方式有哪几种？区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，引入 CSS 的方式多种多样，每种方式都有其适用场景和优缺点。以下是常见的引入方式及其对比：
1. 全局 CSS（Global CSS）
方式
创建独立的 CSS 文件，在入口文件（如index.js）中引入。
所有组件共享样式，通过类名选择器生效。
css
/* styles.css */
.button {
  background-color: blue;
  color: white;
}
jsx
// index.js
import './styles.css';

const App = () => {
  return <button className="button">Click me</button>;
};
特点
优点：简单直接，适合基础样式（如重置样式、全局变量）。
缺点：全局作用域，易导致命名冲突；难以实现样式隔离。
2. CSS Modules
方式
在 CSS 文件名中添加.module.css后缀，使样式局部生效。
通过导入对象访问类名，编译时自动生成唯一类名。
css
/* Button.module.css */
.button {
  background-color: blue;
  color: white;
}
jsx
import styles from './Button.module.css';

const Button = () => {
  return <button className={styles.button}>Click me</button>;
};
特点
优点：局部作用域，避免命名冲突；支持 CSS 特性（如嵌套）。
缺点：需配置构建工具（如 Webpack）；动态样式需通过 JS 管理。
3. 内联样式（Inline Styles）
方式
使用style属性直接在 JSX 中定义样式，值为 JavaScript 对象。
jsx
const Button = () => {
  const buttonStyle = {
    backgroundColor: 'blue',
    color: 'white',
    borderRadius: '4px'
  };
  
  return <button style={buttonStyle}>Click me</button>;
};
特点
优点：作用域局部，动态样式易实现（如基于 props 计算）。
缺点：不支持伪类（如:hover）和媒体查询；样式碎片化，维护成本高。
4. CSS-in-JS 库（如 styled-components、emotion）
方式
使用 JavaScript 创建组件化的样式，支持动态样式和 CSS 特性。
jsx
// styled-components示例
import styled from 'styled-components';

const Button = styled.button‘
  background- color: blue;
                          color: white;
                          border- radius: 4px;
  
  &: hover {
                      background- color: darkblue;
  }
’;

const App = () => {
  return <Button>Click me</Button>;
};
特点
优点：完全局部作用域，支持动态样式、伪类和动画；组件级样式复用。
缺点：学习成本高；需额外依赖；可能影响构建性能。
5. 行内类名（Classnames 库）
方式
使用classnames库动态组合类名，适用于条件渲染场景。
jsx
import classNames from 'classnames';

const Button = ({ isPrimary }) => {
  const buttonClasses = classNames('button', {
    'button-primary': isPrimary,
    'button-secondary': !isPrimary
  });
  
  return <button className={buttonClasses}>Click me</button>;
};
特点
优点：灵活组合类名，适合复杂条件样式；可与其他方式混用。
缺点：依赖额外库；仍需全局 CSS 文件。
6. Tailwind CSS
方式
使用原子化 CSS 类（如bg-blue-500、text-white）直接在 JSX 中组合样式。
jsx
const Button = () => {
  return (
    <button className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded">
      Click me
    </button>
  );
};
特点
优点：无需编写 CSS，减少文件体积；高度可定制；支持响应式设计。
缺点：类名冗长，代码可读性依赖工具支持（如 VSCode 插件）。
7. 对比表格
方式	作用域	动态样式	CSS 特性支持	构建复杂度	性能影响	适用场景
全局 CSS	全局	❌	✅	低	低	基础样式、重置样式
CSS Modules	局部	✅	✅	中	低	组件化样式，避免冲突
内联样式	局部	✅	❌	低	低	简单动态样式
CSS-in-JS	局部	✅	✅	高	中	复杂动态样式、组件库
Classnames 库	全局	✅	✅	中	低	条件组合全局类名
Tailwind CSS	局部	✅	✅	中	低	快速原型、响应式设计
8. 常见面试题扩展
CSS Modules 和 CSS-in-JS 的本质区别是什么？
CSS Modules：通过编译时生成唯一类名实现局部作用域，仍使用 CSS 文件。
CSS-in-JS：完全在 JS 中定义样式，运行时注入<style>标签，支持动态逻辑。
内联样式有哪些性能问题？
每次渲染都会创建新的样式对象，可能触发重排（reflow）。
无法利用浏览器 CSS 引擎的优化（如缓存、并行加载）。
如何处理 React 中的动画效果？
CSS-in-JS 库（如 styled-components）：支持 @keyframes 和 transition。
React Transition Group：专为 React 设计的动画库。
Framer Motion：基于 CSS-in-JS 的高级动画库。
Tailwind CSS 的优缺点？
优点：减少 CSS 文件体积，提高开发速度；避免命名烦恼。
缺点：类名过多导致 HTML 膨胀；调试依赖工具；复杂样式需自定义配置。
9. 最佳实践建议
小型项目：全局 CSS + CSS Modules + Tailwind CSS（快速开发）。
大型项目：CSS-in-JS（如 styled-components 或 emotion）+ Tailwind CSS（基础样式）。
动态样式：优先使用 CSS-in-JS 或 Tailwind 的动态类（如className={isActive ? 'bg-red' : 'bg-blue'}）。
性能敏感场景：避免过多内联样式，使用 CSS Modules 或 CSS-in-JS 的静态样式。
总结
React 中 CSS 引入方式的选择取决于项目规模、团队偏好和性能需求：
全局 CSS和CSS Modules适合传统 CSS 工作流。
CSS-in-JS和Tailwind CSS是现代 React 项目的主流选择。
内联样式和Classnames 库适用于特定场景的补充。
理解各种方式的优缺点，才能在实际开发中做出最合适的技术选型。`
              }
            ]
          },
          {
            question: `10.说说React生命周期有哪些不同阶段?每个阶段对`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React 生命周期分为挂载阶段、更新阶段、卸载阶段和错误处理阶段，每个阶段包含特定的生命周期方法。以下是详细解析：
1. 挂载阶段（Mounting）
组件首次创建并插入 DOM 的过程。
执行顺序
constructor()
作用：初始化 state 和绑定事件处理函数。
注意：避免在构造函数中执行副作用（如 API 请求）。
static getDerivedStateFromProps(props, state)
作用：根据 props 更新 state（罕见场景，如表单回显）。
特性：静态方法，无法访问this。
返回值：返回新的 state 对象或null。
render()
作用：返回 JSX，创建虚拟 DOM。
特性：纯函数，不修改 state。
componentDidMount()
作用：DOM 渲染完成后执行副作用（如 API 请求、订阅、定时器）。
特性：只执行一次，可访问 DOM 节点。
2. 更新阶段（Updating）
组件因props或state变化而重新渲染的过程。
触发条件
props变化（父组件重新渲染）。
this.setState()或forceUpdate()被调用。
执行顺序
static getDerivedStateFromProps(props, state)
同上，每次更新前执行。
shouldComponentUpdate(nextProps, nextState)
作用：控制组件是否需要更新（性能优化）。
默认行为：返回true（强制更新）。
注意：谨慎比较对象引用（推荐使用React.PureComponent）。
render()
同上，生成新的虚拟 DOM。
getSnapshotBeforeUpdate(prevProps, prevState)
作用：在 DOM 更新前捕获当前 DOM 状态（如滚动位置）。
返回值：传递给componentDidUpdate的snapshot参数。
componentDidUpdate(prevProps, prevState, snapshot)
作用：DOM 更新后执行副作用（如对比前后 props 差异，更新 DOM）。
注意：避免在此处直接调用setState()，可能导致无限循环。
3. 卸载阶段（Unmounting）
组件从 DOM 中移除的过程。
执行方法
componentWillUnmount()
作用：清理副作用（如取消订阅、清除定时器、释放资源）。
注意：避免在此处调用setState()，组件已无法更新。
4. 错误处理阶段（Error Handling，自 React 16）
捕获子组件树中错误的过程。
执行方法
static getDerivedStateFromError(error)
作用：在渲染阶段捕获错误，更新 state 显示降级 UI。
特性：静态方法，无法访问this。
componentDidCatch(error, info)
作用：在提交阶段捕获错误，记录错误日志。
注意：仅捕获子组件错误，无法捕获自身、事件处理或异步代码的错误。
5. 生命周期流程图
plaintext
挂载阶段:
constructor() → getDerivedStateFromProps() → render() → componentDidMount()

更新阶段:
getDerivedStateFromProps() → shouldComponentUpdate() → render()
→ getSnapshotBeforeUpdate() → componentDidUpdate()

卸载阶段:
componentWillUnmount()

错误处理:
getDerivedStateFromError() → componentDidCatch()
6. 常见面试题扩展
为什么componentDidMount适合副作用操作？
此时 DOM 已渲染完成，可安全访问 DOM 节点（如初始化第三方库、获取元素尺寸）。
只执行一次，避免重复创建资源（如 API 请求、WebSocket 连接）。
getDerivedStateFromProps和componentDidUpdate的区别？
getDerivedStateFromProps：在渲染前执行，同步更新 state，可能导致额外渲染。
componentDidUpdate：在渲染后执行，可访问 prevProps 和 prevState，适合副作用操作。
如何在函数组件中模拟生命周期？
componentDidMount：useEffect(() => { ... }, [])（依赖项为空）。
componentDidUpdate：useEffect(() => { ... }, [dep])（有依赖项）。
componentWillUnmount：useEffect(() => () => { ... }, [])（返回清理函数）。
为什么不建议在componentDidUpdate直接调用setState？
可能导致无限循环（setState触发更新，更新后又调用setState）。
若需更新 state，应基于prevProps或prevState进行条件判断：
jsx
componentDidUpdate(prevProps) {
  if (prevProps.user.id !== this.props.user.id) {
    this.setState({ ... });
  }
}

7. 生命周期方法弃用说明（React 16.3+）
以下方法已被标记为不安全，建议避免使用：
componentWillMount() → 改用constructor()或componentDidMount()。
componentWillReceiveProps(nextProps) → 改用getDerivedStateFromProps()。
componentWillUpdate(nextProps, nextState) → 改用getSnapshotBeforeUpdate()。
总结
React 生命周期的设计遵循单向数据流和副作用分离原则：
挂载阶段：初始化组件。
更新阶段：高效响应数据变化。
卸载阶段：清理资源防止内存泄漏。
错误处理：增强应用鲁棒性。
理解生命周期各阶段的职责，有助于编写更可靠、更高效的 React 组件。`
              }
            ]
          },
          {
            question: `11.React中组件之间如何通信?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，组件通信是构建复杂应用的基础。根据组件关系（父子、兄弟、跨层级）和数据流向，主要有以下几种通信方式：
1. 父子组件通信
父 → 子：通过 props 传递数据
父组件通过 props 向子组件传递数据或回调函数。
子组件通过this.props（类组件）或参数（函数组件）接收。
jsx
// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  
  return <Child count={count} onIncrement={increment} />;
};

// 子组件
const Child = ({ count, onIncrement }) => {
  return <button onClick={onIncrement}>{count}</button>;
};
子 → 父：通过回调函数传递数据
父组件向子组件传递回调函数。
子组件调用该回调并传入数据。
jsx
// 子组件
const Child = ({ onData }) => {
  const handleClick = () => onData('Hello from child');
  return <button onClick={handleClick}>Send data</button>;
};

// 父组件
const Parent = () => {
  const handleChildData = (data) => console.log(data);
  return <Child onData={handleChildData} />;
};
2. 兄弟组件通信
通过共同父组件中转
兄弟 A通过回调将数据传递给父组件。
父组件更新状态后，将数据传递给兄弟 B。
jsx
// 父组件
const Parent = () => {
  const [message, setMessage] = useState('');
  
  const handleMessage = (newMessage) => setMessage(newMessage);
  
  return (
    <div>
      <SiblingA onSend={handleMessage} />
      <SiblingB message={message} />
    </div>
  );
};
3. 跨层级组件通信（深层嵌套）
方式一：逐层传递 props（Prop Drilling）
数据通过中间层组件逐层传递，代码冗余且维护困难。
方式二：Context API
创建上下文：const MyContext = React.createContext();
提供值：使用<MyContext.Provider value={data}>包裹组件树。
消费值：通过useContext(MyContext)（函数组件）或static contextType（类组件）获取。
jsx
// 创建上下文
const UserContext = React.createContext();

// 顶层组件
const App = () => {
  const user = { name: 'John', age: 30 };
  return (
    <UserContext.Provider value={user}>
      <DeeplyNestedComponent />
    </UserContext.Provider>
  );
};

// 深层组件
const DeeplyNestedComponent = () => {
  const user = useContext(UserContext);
  return <div>Hello, {user.name}</div>;
};
方式三：状态管理库（如 Redux、Zustand）
集中存储状态：所有组件可访问全局状态。
单向数据流：通过 action 触发状态更新。
jsx
// Redux示例
const store = createStore(reducer);

// 组件中使用
const Counter = () => {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  return <button onClick={() => dispatch({ type: 'INCREMENT' })}>{count}</button>;
};
4. 非父子 / 兄弟组件通信（任意组件间）
方式一：发布 - 订阅模式（Event Bus）
创建事件总线：全局事件中心，支持on（监听）和emit（触发）。
jsx
// 创建事件总线
const eventBus = {
  listeners: {},
  on(event, callback) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
  },
  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => callback(data));
    }
  }
};

// 发送组件
const Sender = () => {
  const sendData = () => eventBus.emit('message', 'Hello!');
  return <button onClick={sendData}>Send</button>;
};

// 接收组件
const Receiver = () => {
  useEffect(() => {
    const callback = (data) => console.log('Received:', data);
    eventBus.on('message', callback);
    return () => eventBus.off('message', callback); // 清理监听
  }, []);
  return <div>Listening...</div>;
};
方式二：自定义 Hook 共享状态
创建共享状态的 Hook：封装状态逻辑，多个组件使用同一实例。
jsx
// 创建共享状态的Hook
const useSharedState = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return { count, increment };
};

// 组件A
const ComponentA = () => {
  const { count } = useSharedState();
  return <div>Count: {count}</div>;
};

// 组件B
const ComponentB = () => {
  const { increment } = useSharedState();
  return <button onClick={increment}>Increment</button>;
};
5. 特殊场景通信
方式一：refs 访问 DOM 或组件实例
父组件通过useRef创建 ref，并传递给子组件。
子组件通过forwardRef接收 ref，绑定到 DOM 或方法。
jsx
// 子组件
const Child = forwardRef((props, ref) => {
  const inputRef = useRef();
  const focusInput = () => inputRef.current.focus();
  
  useImperativeHandle(ref, () => ({
    focus: focusInput
  }));
  
  return <input ref={inputRef} />;
});

// 父组件
const Parent = () => {
  const childRef = useRef();
  const handleClick = () => childRef.current.focus();
  
  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};
方式二：路由参数传递（React Router）
通过 URL 参数传递数据（如 ID、查询参数）。
jsx
// 跳转组件
const NavigateButton = () => {
  const navigate = useNavigate();
  return <button onClick={() => navigate('/user/123')}>View User</button>;
};

// 接收组件
const UserPage = () => {
  const { id } = useParams();
  return <div>User ID: {id}</div>;
};
6. 通信方式对比
方式	适用场景	优点	缺点
Props	父子组件	简单直接，类型安全	不适合跨层级
Context API	跨层级组件	避免 Prop Drilling	导致组件复用性降低
状态管理库	复杂应用的全局状态	状态集中管理，便于调试	增加项目复杂度
Event Bus	任意组件间	灵活解耦	难以追踪数据流
自定义 Hook	共享状态逻辑	代码复用，无额外依赖	仅适用于函数组件
refs	访问 DOM 或调用子组件方法	直接操作，性能优化	破坏单向数据流
路由参数	页面间传递数据	刷新后数据仍保留	仅适合简单数据
7. 最佳实践建议
优先使用 props：简单场景首选，保持数据流向清晰。
谨慎使用 Context：避免滥用，仅用于真正全局的数据（如主题、用户信息）。
状态管理库：大型应用或复杂状态逻辑（如购物车、用户认证）。
避免 Event Bus：可能导致代码难以维护，优先考虑其他方案。
限制 refs 使用：仅用于 DOM 操作（如滚动、焦点），避免用于数据传递。
总结
React 组件通信的核心原则是单向数据流和状态提升：
就近管理状态：将状态放在最近的共同父组件中。
选择合适的工具：根据组件关系和数据复杂度选择通信方式。
避免过度设计：优先使用简单方案，必要时引入复杂度。
掌握各种通信方式的优缺点，才能在实际开发中做出最优选择。`
              }
            ]
          },
          {
            question: `12.说说对高阶组件的理解？应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `高阶组件（Higher-Order Component, HOC）是 React 中复用组件逻辑的高级技术，它本身不是组件，而是一个函数，接收组件作为参数并返回新组件。以下是详细解析：
1. 基本概念
定义
高阶组件（HOC）：const EnhancedComponent = higherOrderComponent(WrappedComponent);
作用：复用代码、状态管理、渲染劫持、代码分割等。
简单示例
jsx
// HOC：添加日志功能
const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(‘Component ￥{ WrappedComponent.name } mounted’);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// 使用HOC增强组件
const MyComponent = (props) => <div>Hello {props.name}</div>;
const EnhancedComponent = withLogging(MyComponent);
2. 实现方式
方式一：属性代理（Props Proxy）
操作 props：增强组件可以修改、添加或过滤 props。
示例：
jsx
const withExtraProps = (WrappedComponent) => {
  return (props) => (
    <WrappedComponent 
      {...props} 
      extraProp="额外属性" 
    />
  );
};

方式二：反向继承（Inheritance Inversion）
继承 WrappedComponent：增强组件继承自被包裹组件。
示例：
jsx
const withStateLogging = (WrappedComponent) => {
  return class extends WrappedComponent {
    componentDidUpdate(prevProps, prevState) {
      console.log('Prev state:', prevState);
      console.log('New state:', this.state);
      super.componentDidUpdate(prevProps, prevState);
    }
    render() {
      return super.render();
    }
  };
};

3. 核心应用场景
场景一：代码复用（状态 / 逻辑共享）
示例：多个组件需要相同的 API 数据加载逻辑。
jsx
const withData = (WrappedComponent, apiUrl) => {
  return class extends React.Component {
    state = { data: null, loading: true };
    
    async componentDidMount() {
      const response = await fetch(apiUrl);
      const data = await response.json();
      this.setState({ data, loading: false });
    }
    
    render() {
      return (
        <WrappedComponent 
          {...this.props} 
          data={this.state.data} 
          loading={this.state.loading} 
        />
      );
    }
  };
};

场景二：状态管理（如 Redux 的 connect）
示例：将组件连接到 Redux store。
jsx
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  return class extends React.Component {
    render() {
      const state = mapStateToProps(store.getState());
      const dispatch = mapDispatchToProps(store.dispatch);
      return <WrappedComponent {...this.props} {...state} {...dispatch} />;
    }
  };
};

场景三：条件渲染（权限控制）
示例：根据用户权限决定是否渲染组件。
jsx
const withAuth = (WrappedComponent) => {
  return (props) => {
    if (!props.user.isAdmin) {
      return <div>无权访问</div>;
    }
    return <WrappedComponent {...props} />;
  };
};

场景四：性能优化（代码分割）
示例：懒加载组件。
jsx
const withLazyLoading = (importComponent) => {
  return class extends React.Component {
    state = { Component: null };
    
    async componentDidMount() {
      const { default: Component } = await importComponent();
      this.setState({ Component });
    }
    
    render() {
      const { Component } = this.state;
      return Component ? <Component {...this.props} /> : <div>Loading...</div>;
    }
  };
};

场景五：渲染劫持（修改 UI）
示例：根据条件修改渲染内容。
jsx
const withErrorBoundary = (WrappedComponent) => {
  return class extends React.Component {
    state = { hasError: false };
    
    static getDerivedStateFromError() {
      return { hasError: true };
    }
    
    render() {
      if (this.state.hasError) {
        return <div>Something went wrong.</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
};

4. 与其他模式的对比
HOC vs Render Props
HOC：通过返回新组件封装逻辑，组件嵌套层级增加。
Render Props：通过 props 传递函数，避免额外层级，但可能导致回调地狱。
HOC vs Hooks
HOC：适合跨组件复用复杂逻辑（如状态管理、生命周期操作）。
Hooks：更简洁，无额外组件层级，推荐优先使用（如useState、useEffect）。
5. 优缺点
优点
代码复用：避免重复逻辑，提高可维护性。
关注点分离：将特定功能封装在 HOC 中，组件专注于 UI。
可组合性：多个 HOC 可嵌套使用（如withAuth(withData(Component))）。
缺点
命名冲突：多个 HOC 可能向组件注入相同 props。
层级嵌套：过多 HOC 会导致组件树层级过深，调试困难。
静态属性丢失：需手动复制静态属性（如displayName）。
Refs 无法穿透：HOC 会包裹原始组件，ref 指向 HOC 而非原始组件。
6. 常见面试题扩展
如何解决 HOC 的静态属性丢失问题？
使用hoist-non-react-statics自动复制所有静态属性：
jsx
import hoistNonReactStatics from 'hoist-non-react-statics';

const withData = (WrappedComponent) => {
  const EnhancedComponent = ...;
  return hoistNonReactStatics(EnhancedComponent, WrappedComponent);
};

如何在 HOC 中转发 refs？
使用React.forwardRef：
jsx
const withLogging = (WrappedComponent) => {
  const LoggingComponent = React.forwardRef((props, ref) => (
    <WrappedComponent ref={ref} {...props} />
  ));
  return LoggingComponent;
};

HOC 与普通组件的区别？
普通组件：将 props 转换为 UI。
HOC：是一个函数，将组件转换为另一个组件，不直接产生 UI。
HOC 在 React Hooks 时代是否过时？
未过时，但 Hooks 提供了更简洁的逻辑复用方式。HOC 仍适用于：
需要修改组件结构的场景（如条件渲染）。
与旧代码兼容。
实现复杂的状态管理逻辑（如 Redux 的connect）。
7. 最佳实践
保持单一职责：每个 HOC 专注于一个特定功能。
使用函数式 HOC：无状态 HOC 更简单、更易测试。
避免多层嵌套：过多 HOC 会导致调试困难，考虑使用 Hooks 替代。
显式命名：使用displayName标识增强组件，便于调试。
jsx
const withData = (WrappedComponent) => {
  const EnhancedComponent = (props) => ...;
  EnhancedComponent.displayName = ‘withData(￥{ WrappedComponent.displayName || WrappedComponent.name || 'Component' })’;
  return EnhancedComponent;
};


总结
高阶组件是 React 中强大的抽象工具，核心价值在于组件逻辑复用和关注点分离：
适用场景：代码复用、状态管理、权限控制、性能优化等。
与 Hooks 的关系：Hooks 解决了 HOC 的部分痛点（如层级嵌套），但 HOC 仍有不可替代的场景。
注意事项：避免过度使用，警惕命名冲突和性能问题。
理解 HOC 的设计思想，有助于更好地组织和优化 React 应用。`
              }
            ]
          },
          {
            question: `13.在react中组件间过渡动画如何实现？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中实现组件间过渡动画主要有以下几种方式，每种方式适用于不同场景：
1. CSS Transitions/Animations + 条件渲染
通过控制组件的显示 / 隐藏，结合 CSS 过渡效果实现动画。
示例：淡入淡出效果
jsx
import { useState } from 'react';
import './Fade.css';

const Fade = () => {
  const [show, setShow] = useState(true);
  
  const toggle = () => setShow(!show);
  
  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      {show && (
        <div className="fade-animation">
          This element will fade in and out
        </div>
      )}
    </div>
  );
};
css
/* Fade.css */
.fade-animation {
  opacity: 1;
  transition: opacity 0.5s ease-in-out;
}

.fade-animation:not(:last-child) {
  opacity: 0;
  height: 0;
  overflow: hidden;
}
2. React Transition Group
React 官方提供的动画库，专门处理组件的进入 / 离开状态。
安装
bash
npm install react-transition-group
示例：使用 CSSTransition
jsx
import { useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './Fade.css';

const FadeTransition = () => {
  const [show, setShow] = useState(true);
  
  return (
    <div>
      <button onClick={() => setShow(!show)}>Toggle</button>
      <CSSTransition
        in={show}
        timeout={500}
        classNames="fade"
        unmountOnExit
      >
        <div className="box">Fading element</div>
      </CSSTransition>
    </div>
  );
};
css
/* Fade.css */
.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  opacity: 1;
  transition: opacity 500ms;
}
.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  opacity: 0;
  transition: opacity 500ms;
}
3. React Transition Group - TransitionGroup
用于管理多个元素的动画（如列表项的添加 / 删除）。
示例：列表项动画
jsx
import { useState } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';

const ListAnimation = () => {
  const [items, setItems] = useState([1, 2, 3]);
  
  const addItem = () => {
    setItems([...items, Math.max(0, ...items) + 1]);
  };
  
  const removeItem = (id) => {
    setItems(items.filter(item => item !== id));
  };
  
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <TransitionGroup component="ul">
        {items.map(item => (
          <CSSTransition
            key={item}
            timeout={500}
            classNames="item"
          >
            <li onClick={() => removeItem(item)}>
              Item {item}
            </li>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
};
4. Framer Motion
功能强大的动画库，支持复杂交互和物理动画效果。
安装
bash
npm install framer-motion
示例：元素入场动画
jsx
import { motion } from 'framer-motion';

const MotionExample = () => {
  const variants = {
    hidden: { opacity: 0, y: -20 },
    visible: { opacity: 1, y: 0 }
  };
  
  return (
    <motion.div
      initial="hidden"
      animate="visible"
      transition={{ duration: 0.5 }}
      variants={variants}
    >
      Animated with Framer Motion
    </motion.div>
  );
};
示例：组件切换动画
jsx
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const PageTransition = () => {
  const [page, setPage] = useState(1);
  
  const togglePage = () => {
    setPage(page === 1 ? 2 : 1);
  };
  
  return (
    <div>
      <button onClick={togglePage}>Switch Page</button>
      <AnimatePresence>
        {page === 1 ? (
          <motion.div
            key="page1"
            initial={{ opacity: 0, x: -20 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 20 }}
            transition={{ duration: 0.3 }}
          >
            Page 1 Content
          </motion.div>
        ) : (
          <motion.div
            key="page2"
            initial={{ opacity: 0, x: 20 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: -20 }}
            transition={{ duration: 0.3 }}
          >
            Page 2 Content
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
};
5. React Spring
基于物理的动画库，提供自然流畅的动画效果。
安装
bash
npm install react-spring
示例：弹性动画
jsx
import { useSpring, animated } from 'react-spring';

const SpringExample = () => {
  const props = useSpring({
    opacity: 1,
    from: { opacity: 0 },
    config: { tension: 170, friction: 26 }
  });
  
  return <animated.div style={props}>Animated with React Spring</animated.div>;
};
6. 自定义 Hook 实现简单动画
使用 useState 和 useEffect 创建自定义动画逻辑。
示例：自定义淡入淡出 Hook
jsx
import { useState, useEffect } from 'react';

const useFadeIn = (duration = 500) => {
  const [opacity, setOpacity] = useState(0);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setOpacity(1);
    }, 10);
    
    return () => clearTimeout(timer);
  }, [duration]);
  
  return {
    style: { opacity, transition: ‘opacity ￥{ duration }ms ease -in -out’ }
  };
};

const CustomHookExample = () => {
  const { style } = useFadeIn(1000);
  
  return <div style={style}>Fade in with custom hook</div>;
};
7. 性能优化建议
优先使用 transform 和 opacity：这两个属性不会触发重排（reflow），性能最优。
避免过多 DOM 操作：动画过程中频繁修改 DOM 结构会导致性能下降。
使用 will-change：提前告知浏览器元素即将变化，优化渲染性能。
css
.element {
  will-change: transform, opacity;
}


控制动画时长：过长的动画会让用户感觉页面卡顿，通常 300-500ms 为宜。
8. 选择合适的动画库
简单 CSS 动画：适合基础过渡效果，无需额外依赖。
React Transition Group：适合组件进入 / 离开动画，官方维护，生态稳定。
Framer Motion：适合复杂交互和高级动画，API 设计友好，文档完善。
React Spring：适合物理动画和精细控制，性能优秀。
根据项目需求和团队技术栈选择合适的方案，避免过度引入复杂度。`
              }
            ]
          },
          {
            question: `14.说说你在React项目是如何捕获错误的?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 项目中，错误捕获是保障应用稳定性的关键环节。根据错误类型（渲染错误、生命周期错误、异步错误等），可采用不同的捕获策略：
1. 错误边界（Error Boundaries，自 React 16）
作用
捕获并处理子组件树中的渲染时错误、生命周期错误和构造函数错误。
防止整个应用崩溃，显示降级 UI。
实现方式
类组件：通过static getDerivedStateFromError()和componentDidCatch()实现。
jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  // 渲染降级UI
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  // 记录错误日志
  componentDidCatch(error, errorInfo) {
    console.error('Error Boundary caught:', error, errorInfo);
    // 可发送错误到日志服务
    Sentry.captureException(error);
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary>
  <ComponentThatMayCrash />
</ErrorBoundary>
局限性
无法捕获事件处理、异步代码（如 setTimeout、Promise）、服务端渲染和自身错误。
2. 事件处理中的错误捕获
方式
使用try...catch包裹事件处理函数。
自定义错误处理函数。
jsx
const MyComponent = () => {
  const handleClick = () => {
    try {
      // 可能出错的代码
      throw new Error('Event handler error');
    } catch (error) {
      console.error('Caught in event handler:', error);
      // 显示错误提示或记录日志
    }
  };

  return <button onClick={handleClick}>Click</button>;
};
3. 异步代码中的错误捕获
Promise 错误
使用.catch()捕获 Promise 错误。
全局 Promise 错误监听（不推荐，仅作兜底）。
jsx
const fetchData = async () => {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('API error:', error);
    throw error; // 可继续向上抛出
  }
};

// 全局Promise错误监听
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Promise rejection:', event.reason);
  event.preventDefault(); // 阻止默认行为（如控制台警告）
});
React Suspense 与 Error Boundaries
对于 React.lazy 加载的组件，使用 Suspense 和 Error Boundaries 组合。
jsx
const LazyComponent = React.lazy(() => import('./LazyComponent'));

<ErrorBoundary>
  <React.Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </React.Suspense>
</ErrorBoundary>
4. 全局错误监听
window.onerror
捕获未处理的 JavaScript 运行时错误。
jsx
window.onerror = (message, source, lineno, colno, error) => {
  console.error('Global error:', message, error);
  // 发送错误日志到服务端
  return true; // 阻止错误冒泡
};
window.addEventListener('error')
捕获资源加载错误（如图片、脚本加载失败）。
jsx
window.addEventListener('error', (event) => {
  if (event instanceof ErrorEvent) {
    console.error('Script error:', event.error);
  } else {
    console.error('Resource loading error:', event.target);
  }
  event.preventDefault();
});
5. 第三方错误监控工具
Sentry
集成简单，提供详细错误堆栈和性能监控。
bash
npm install @sentry/react @sentry/tracing
jsx
// 初始化
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  integrations: [new BrowserTracing()],
  tracesSampleRate: 1.0,
});

// 在ErrorBoundary中使用
componentDidCatch(error, errorInfo) {
  Sentry.captureException(error);
  // ...
}
TrackJS
专注于前端错误监控，提供用户行为追踪。
6. 测试环境错误捕获
Jest + React Testing Library
使用@testing-library/jest-dom的toThrowError匹配器。
jsx
test('renders error boundary on crash', () => {
  const Wrapper = () => (
    <ErrorBoundary>
      <ComponentThatThrowsError />
    </ErrorBoundary>
  );

  expect(() => render(<Wrapper />)).toThrowError('Expected error');
});
7. 错误处理最佳实践
分层捕获：
全局错误监听（window.onerror）作为最后一道防线。
关键组件使用 Error Boundary 包裹。
异步代码使用 try...catch 或.catch ()。
友好的错误提示：
错误边界提供用户可理解的提示（如 “加载失败，请重试”）。
避免暴露详细技术信息给用户。
错误日志分析：
记录错误堆栈、用户操作路径和环境信息。
使用工具（如 Sentry）分析错误趋势和优先级。
避免静默失败：
不要在 catch 块中忽略错误（至少记录日志）。
使用 TypeScript 等工具提前发现潜在错误。
8. 常见面试题扩展
Error Boundaries 能否捕获所有错误？
不能。它只能捕获渲染期间、生命周期方法和构造函数中的错误。无法捕获：
事件处理（如 onClick）。
异步代码（如 setTimeout、Promise）。
服务端渲染。
Error Boundary 自身的错误。
如何测试 Error Boundaries？
使用 React Testing Library 触发错误，验证降级 UI 是否显示：
jsx
test('shows fallback UI when component crashes', () => {
  const { getByText } = render(
    <ErrorBoundary>
      <ComponentThatThrows />
    </ErrorBoundary>
  );
  expect(getByText('Something went wrong')).toBeInTheDocument();
});

React 18 中的错误处理有什么变化？
没有重大变化，但 Concurrent 模式下错误可能在不同阶段被捕获，需确保 Error Boundary 正确处理异步渲染。
如何处理自定义 Hook 中的错误？
在 Hook 内部使用 try...catch 捕获错误，并通过回调或状态暴露给调用组件。
或让调用组件使用 Error Boundary 包裹使用该 Hook 的组件。
总结
React 项目中的错误捕获需采用多层防御策略：
Error Boundaries：处理渲染和生命周期错误。
try...catch：处理事件和异步代码错误。
全局错误监听：兜底未捕获的错误。
第三方工具：增强错误收集和分析能力。
合理的错误处理不仅能提升用户体验，还能加速问题定位和修复，是高质量 React 应用的重要组成部分。`
              }
            ]
          },
          {
            question: `15.说说对React refs的理解?应用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，Refs（引用）是一种用于访问 DOM 节点或组件实例的机制。它突破了 React 单向数据流的限制，允许直接操作 DOM 或与外部库集成。以下是详细解析：
1. 基本概念
作用
访问 DOM 节点（如获取尺寸、触发焦点、滚动等）。
调用子组件的方法（类组件或 forwardRef 组件）。
存储不需要触发渲染的可变数据（替代 useState 导致的重复渲染）。
创建方式
类组件：this.refs.myRef（旧）或React.createRef()。
函数组件：useRef() Hook。
使用场景
管理焦点、文本选择或媒体播放。
触发强制动画。
集成第三方 DOM 库（如 D3、Chart.js）。
2. 创建和使用 Refs
类组件示例
jsx
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef(); // 创建ref
  }

  componentDidMount() {
    this.myRef.current.focus(); // 访问DOM节点
  }

  render() {
    return <input ref={this.myRef} />;
  }
}
函数组件示例
jsx
const MyComponent = () => {
  const myRef = useRef(null); // 创建ref

  useEffect(() => {
    myRef.current.focus(); // 访问DOM节点
  }, []);

  return <input ref={myRef} />;
};
3. Ref 转发（Forwarding Refs）
作用
让父组件能够访问子组件的 DOM 节点。
常用于高阶组件和可复用组件库。
示例
jsx
// 子组件：使用forwardRef
const CustomInput = React.forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

// 父组件：访问子组件的ref
const Parent = () => {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus(); // 直接访问子组件的DOM
  };

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
};
4. 回调 Refs
作用
通过函数而非对象管理 ref。
更细粒度地控制 ref 的挂载和卸载。
示例
jsx
const MyComponent = () => {
  let myRef = null;

  const setRef = (node) => {
    myRef = node; // 手动存储ref
    if (node) {
      node.focus(); // ref挂载后执行操作
    }
  };

  return <input ref={setRef} />;
};
5. useRef 的非 DOM 用途
存储可变值
替代useState存储不需要触发渲染的数据（如计时器 ID、API 请求状态）。
jsx
const TimerComponent = () => {
  const timerRef = useRef(null);

  const startTimer = () => {
    timerRef.current = setInterval(() => {
      console.log('Timer running...');
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(timerRef.current);
  };

  useEffect(() => {
    return () => stopTimer(); // 组件卸载时清理
  }, []);

  return (
    <div>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
};
跳过重复计算
缓存计算结果，避免重复执行昂贵操作。
jsx
const ExpensiveComponent = () => {
  const resultRef = useRef();

  if (!resultRef.current) {
    resultRef.current = performExpensiveCalculation();
  }

  return <div>{resultRef.current}</div>;
};
6. 注意事项
避免过度使用：Refs 打破了 React 的数据流模式，应优先使用状态管理（如 props、useState）。
不触发渲染：修改 ref.current 不会触发组件重新渲染。
生命周期问题：
类组件中，ref 在componentDidMount和componentDidUpdate后可用。
函数组件中，避免在渲染期间访问 ref（可能导致不一致）。
DOM 操作限制：
避免在 ref 中修改样式（优先使用 state 和 CSS）。
仅用于必须直接操作 DOM 的场景（如滚动、焦点）。
7. 常见面试题扩展
React 为什么推荐避免直接操作 DOM？
React 基于虚拟 DOM 和单向数据流，直接操作 DOM 会破坏其设计理念，导致状态不一致和调试困难。
Refs 应作为最后的手段，仅用于无法通过声明式实现的场景。
useRef 和 useState 的区别？
useState：修改值会触发重新渲染，适用于驱动 UI 更新的数据。
useRef：修改值不会触发渲染，适用于存储不影响 UI 的可变数据（如计时器、DOM 引用）。
如何在函数组件中访问子组件的方法？
子组件使用React.forwardRef和useImperativeHandle暴露方法：
jsx
const Child = React.forwardRef((props, ref) => {
  const handleClick = () => { /* ... */ };
  
  useImperativeHandle(ref, () => ({
    click: handleClick
  }));
  
  return <button>Child Button</button>;
});

// 父组件
const Parent = () => {
  const childRef = useRef();
  return (
    <div>
      <Child ref={childRef} />
      <button onClick={() => childRef.current.click()}>Trigger Child</button>
    </div>
  );
};

Refs 在 Concurrent 模式下有什么变化？
React 18 的并发渲染可能导致渲染过程被暂停或重启，需注意：
避免在渲染期间访问 ref（可能指向旧的 DOM）。
使用useLayoutEffect而非useEffect访问 ref（确保 DOM 已更新）。
8. 应用场景总结
场景	示例
DOM 操作	焦点控制、滚动到特定位置、测量 DOM 尺寸。
触发强制动画	使用 requestAnimationFrame 或 CSS 动画库操作 DOM。
集成第三方库	D3、Chart.js 等需要直接访问 DOM 的库。
存储临时数据	计时器 ID、API 请求状态、表单值缓存（不触发渲染）。
访问子组件方法	通过 forwardRef 和 useImperativeHandle 暴露子组件的方法（类组件或函数组件）。
优化性能	缓存昂贵计算结果，避免重复执行。
总结
Refs 是 React 提供的一种强大但需谨慎使用的特性，它允许突破单向数据流的限制，直接访问 DOM 或组件实例：
核心用途：DOM 操作、子组件方法调用、存储可变数据。
最佳实践：仅在必要时使用，优先采用声明式解决方案。
未来趋势：在 Concurrent 模式下，需注意 ref 访问的时机和一致性。
理解 Refs 的适用场景和限制，有助于编写更高效、更可维护的 React 代码。`
              }
            ]
          },
          {
            question: `16.说说React中的setState执行机制`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，setState是更新组件状态的核心方法，但其执行机制涉及批量更新、异步行为和优先级调度等复杂逻辑。以下是详细解析：
1. 基本概念
异步更新
setState是异步的：React 可能会合并多次setState调用以提高性能。
无法立即获取更新后的状态：
jsx
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 可能仍为旧值

批量更新
在同一事件循环中多次调用setState，React 会合并为一次更新：
jsx
// 只触发一次重新渲染，count最终为1
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });

2. 执行机制详解
两个阶段
状态合并：将新状态与当前状态合并（浅合并）。
组件更新：触发shouldComponentUpdate、render等生命周期。
异步原因
性能优化：批量处理多个状态更新，减少渲染次数。
一致性保证：在同一事件中保持状态的一致性（避免中间状态导致的 UI 闪烁）。
3. 获取更新后的状态
回调函数
通过第二个参数传递回调：
jsx
this.setState(
  { count: this.state.count + 1 },
  () => console.log(this.state.count) // 输出更新后的值
);

useEffect（函数组件）
函数组件中使用useEffect监听状态变化：
jsx
const [count, setCount] = useState(0);

const increment = () => {
  setCount(count + 1);
};

useEffect(() => {
  console.log('Count updated:', count); // 每次更新后执行
}, [count]);

4. 函数式更新
场景
当新状态依赖于旧状态时，应使用函数式更新：
jsx
// 错误：可能丢失更新（多次调用时）
this.setState({ count: this.state.count + 1 });

// 正确：确保基于最新状态计算
this.setState((prevState) => ({
  count: prevState.count + 1
}));

原因
异步更新导致this.state可能不是最新值。
函数式更新会被加入队列，按顺序执行。
5. 批量更新规则
React 事件处理中
批量执行，合并多次setState：
jsx
handleClick = () => {
  this.setState({ count: this.state.count + 1 }); // 不立即更新
  this.setState({ count: this.state.count + 1 }); // 合并为一次更新
  console.log(this.state.count); // 输出旧值
};

非 React 事件中
同步执行（如 setTimeout、Promise、原生 DOM 事件）：
jsx
componentDidMount() {
  setTimeout(() => {
    this.setState({ count: this.state.count + 1 }); // 立即更新
    this.setState({ count: this.state.count + 1 }); // 两次独立更新
    console.log(this.state.count); // 输出2
  }, 0);
}

React 18 + 的自动批处理
在所有场景（包括 Promise、setTimeout）中都默认批量处理：
jsx
// React 18+中，即使在setTimeout中也会批量更新
setTimeout(() => {
  setCount(c => c + 1);
  setCount(c => c + 1); // 只触发一次渲染
}, 0);

6. 性能优化
浅层比较
React 通过浅层比较（shallow comparison）判断是否需要更新：
对象引用变化才会触发更新。
避免直接修改状态对象：
jsx
// 错误：直接修改原对象
this.state.items.push(newItem);
this.setState({ items: this.state.items }); // 不触发更新

// 正确：创建新对象
this.setState({ items: [...this.state.items, newItem] });

shouldComponentUpdate
类组件中可通过shouldComponentUpdate阻止不必要的更新：
jsx
shouldComponentUpdate(nextProps, nextState) {
  return nextState.count !== this.state.count;
}

React.memo（函数组件）
浅比较 props，避免重复渲染：
jsx
const MyComponent = React.memo(({ count }) => {
  return <div>{count}</div>;
});

7. 常见面试题扩展
为什么 setState 是异步的？
性能优化：批量处理多个状态更新，减少 DOM 操作次数。
一致性保证：确保在同一事件中所有组件的状态更新是一致的。
如何在 setState 后立即获取更新后的状态？
使用回调函数：this.setState({...}, () => {...})。
函数组件中使用useEffect。
setState 和 useState 的区别？
setState（类组件）：
可合并多个状态更新。
支持函数式更新。
useState（函数组件）：
每次调用都会替换原有状态（除非使用函数式更新）。
基本类型状态不自动合并，对象需手动合并：
jsx
const [state, setState] = useState({ name: '', age: 0 });
setState(prev => ({ ...prev, name: 'John' })); // 手动合并

React 18 的自动批处理有什么影响？
在所有场景（包括 Promise、setTimeout）中都默认批量处理，可能导致现有代码行为变化：
jsx
// React 18+中，即使在Promise中也会批量更新
Promise.resolve().then(() => {
  setCount(c => c + 1);
  setCount(c => c + 1); // 只触发一次渲染
});

8. 最佳实践
优先使用函数式更新：当新状态依赖于旧状态时。
避免直接修改状态：始终创建新对象 / 数组。
合理使用批量更新：在需要多次更新时，利用批量特性减少渲染。
按需获取更新后状态：使用回调或 useEffect。
性能敏感场景优化：通过shouldComponentUpdate或React.memo避免不必要的渲染。
总结
setState的执行机制是 React 性能优化的核心之一，理解其异步、批量特性对于编写高效、稳定的 React 应用至关重要：
异步特性：出于性能和一致性考虑，React 可能合并多次状态更新。
函数式更新：确保在复杂场景下状态更新的正确性。
批量规则：在 React 事件中批量处理，非 React 事件中可能同步处理（React 18 + 默认批量）。
性能优化：利用浅层比较和避免不必要的渲染。
掌握setState的工作原理，能够帮助开发者避免常见陷阱，编写出更健壮的 React 代码。`
              }
            ]
          },
          {
            question: `17.说说Reactrender方法的原理?在什么时候会被触发？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，render方法是组件的核心，负责将组件状态转换为虚拟 DOM。以下是其原理和触发条件的详细解析：
1. render 方法的核心作用
生成虚拟 DOM
纯函数：接收this.props和this.state，返回 React 元素（虚拟 DOM）。
不修改状态：不直接改变组件状态，仅描述 UI 应呈现的样子。
jsx
// 类组件的render方法
class MyComponent extends React.Component {
  render() {
    return <div>{this.props.message}</div>;
  }
}

// 函数组件（本质是render函数）
const MyComponent = (props) => {
  return <div>{props.message}</div>;
};
2. render 方法的执行原理
虚拟 DOM 的生成
调用 render：React 在需要更新组件时调用render方法。
生成虚拟 DOM 树：render返回的 JSX 被转换为轻量级 JavaScript 对象（虚拟 DOM）。
Diff 算法：React 将新生成的虚拟 DOM 与旧版本对比，找出最小变更集。
DOM 更新：仅将必要的真实 DOM 变更应用到页面。
调和（Reconciliation）过程
调和：React 比较新旧虚拟 DOM 的过程，决定如何高效更新真实 DOM。
render方法是调和过程的起点，但实际 DOM 更新由 React 控制。
3. render 方法的触发条件
类组件
初始渲染：组件实例创建后首次调用。
state 变化：this.setState()或this.forceUpdate()被调用。
props 变化：父组件重新渲染导致 props 更新。
父组件渲染：无论 props 是否变化，父组件渲染时子组件都会重新调用render。
函数组件
初始渲染：组件首次被调用。
state 变化：调用useState的更新函数。
props 变化：父组件传递新的 props。
context 变化：组件订阅的 context 值发生变化。
4. 避免不必要的 render 调用
类组件优化
shouldComponentUpdate：手动控制组件是否需要更新。
jsx
shouldComponentUpdate(nextProps, nextState) {
  return nextState.count !== this.state.count; // 仅在count变化时更新
}

React.PureComponent：自动浅比较 props 和 state，避免不必要的 render。
函数组件优化
React.memo：高阶组件，浅比较 props，阻止重复渲染。
jsx
const MyComponent = React.memo(({ count }) => {
  return <div>{count}</div>;
});

useMemo/useCallback：缓存计算结果或函数引用。
5. render 方法的注意事项
纯函数特性：
不修改 props 或 state。
相同输入必须返回相同输出。
避免在 render 中执行副作用（如 API 请求）。
性能影响：
频繁调用 render 会影响性能，但虚拟 DOM 的 Diff 算法已优化此过程。
避免在 render 中创建新对象或函数（可能导致不必要的重新渲染）。
与生命周期的关系：
在类组件中，render 后依次触发componentDidMount/componentDidUpdate。
函数组件中，render 后触发useEffect。
6. 常见面试题扩展
render 方法可以返回什么？
React 元素（JSX）。
数组或 Fragment。
字符串、数字、null 或布尔值。
Portal（通过React.createPortal）。
render 方法和虚拟 DOM 的关系？
render方法生成虚拟 DOM 树。
虚拟 DOM 是轻量级 JavaScript 对象，是真实 DOM 的抽象表示。
React 通过比较新旧虚拟 DOM，最小化真实 DOM 操作。
React 18 的并发渲染如何影响 render？
并发渲染允许 React 中断、恢复或重新执行 render。
render可能被多次调用，但实际 DOM 更新仅在最终确定后执行。
需确保 render 是纯函数，避免副作用。
如何测试 render 方法？
使用 React Testing Library 渲染组件，验证输出：
jsx
test('renders message', () => {
  const { getByText } = render(<MyComponent message="Hello" />);
  expect(getByText('Hello')).toBeInTheDocument();
});

7. 总结
render方法是 React 组件的核心，其执行机制和触发条件直接影响应用性能：
核心作用：生成虚拟 DOM，描述 UI 结构。
触发条件：state 变化、props 更新、父组件渲染等。
性能优化：通过shouldComponentUpdate、React.memo等避免不必要的渲染。
纯函数要求：确保 render 不产生副作用，保持一致性。
理解render的工作原理，有助于编写高效、可维护的 React 代码。`
              }
            ]
          },
          {
            question: `18.说说 Real DOM和Virtual DOM的区别?优缺点?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 等前端框架中，Real DOM（真实 DOM） 和Virtual DOM（虚拟 DOM） 是实现页面渲染的两种核心机制，它们在性能、原理和使用场景上有显著区别。以下从定义、区别、优缺点三个方面详细解析：
一、定义
Real DOM：浏览器中真实存在的 DOM 节点，是 HTML 文档的树形结构表示，可通过 JavaScript（如document.getElementById()）直接操作，修改后会触发页面重排（Reflow）或重绘（Repaint）。
Virtual DOM：用 JavaScript 对象模拟的 DOM 结构（轻量级副本），仅包含真实 DOM 的关键信息（如标签名、属性、子节点等），不涉及浏览器渲染逻辑，修改时不会直接影响页面。
二、核心区别
维度	Real DOM	Virtual DOM
本质	浏览器渲染引擎管理的真实节点	JavaScript 对象（模拟 DOM 的树形结构）
操作成本	直接操作真实 DOM，成本高（重排 / 重绘耗时）	操作 JS 对象，成本低（无页面渲染开销）
更新方式	修改后立即反映到页面	先修改虚拟 DOM，再通过 Diff 算法计算差异后批量更新真实 DOM
跨平台性	依赖浏览器环境，无法跨平台	与平台无关，可用于 React Native 等非浏览器环境
内存占用	较高（包含浏览器渲染相关属性和方法）	较低（仅保留必要信息）
三、优缺点分析
1. Real DOM 的优缺点
优点：
直接性：可通过原生 API 直接操作，无需中间层转换。
实时性：修改后立即反映到页面，适合简单场景。
缺点：
性能开销大：每次修改都会触发重排 / 重绘（浏览器重新计算布局和绘制页面），频繁操作会导致页面卡顿。
冗余属性多：真实 DOM 包含大量浏览器渲染相关的属性和方法（如offsetHeight、getBoundingClientRect），内存占用高。
跨平台受限：仅能在浏览器环境中使用，无法适配移动端（如 React Native）。
2. Virtual DOM 的优缺点
优点：
性能优化：通过Diff 算法对比新旧虚拟 DOM，计算最小变更集，最终仅更新必要的真实 DOM，减少重排 / 重绘次数。
开发效率高：开发者无需手动优化 DOM 操作，框架自动处理更新逻辑。
跨平台能力：虚拟 DOM 是 JS 对象，可被渲染到不同平台（如浏览器 DOM、移动端原生组件、Canvas 等），这也是 React Native 的核心原理。
稳定性：避免直接操作真实 DOM 导致的意外问题（如误删节点、属性冲突）。
缺点：
额外开销：需要维护虚拟 DOM 副本，且 Diff 算法本身有一定计算成本（虽然远低于真实 DOM 操作）。
非实时更新：虚拟 DOM 的更新是 “批量异步” 的，修改后不会立即反映到页面，可能导致短暂的数据与 UI 不一致（React 通过setState的异步更新机制处理）。
过度优化：对于简单场景（如单个 DOM 节点修改），虚拟 DOM 的 Diff 和批量更新可能反而增加不必要的步骤。
四、总结
Real DOM 是浏览器原生的渲染载体，适合简单、少量的 DOM 操作，但频繁修改会导致性能问题。
Virtual DOM 是框架层面的优化方案，通过 “先模拟、再计算差异、最后批量更新” 的方式，解决了真实 DOM 操作的性能瓶颈，同时支持跨平台开发，是现代前端框架（如 React、Vue）的核心特性。
在面试中，需重点强调虚拟 DOM 的 Diff 算法和批量更新机制，以及它如何平衡开发效率与性能 —— 这也是 React 等框架选择虚拟 DOM 的核心原因。`
              }
            ]
          },
          {
            question: `19.说说ReactJsx转换成真实DOM过程?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，JSX 到真实 DOM 的转换是一个涉及编译、虚拟 DOM 和调和算法的复杂过程。以下是详细解析：
1. JSX 编译为 React 元素
Babel 编译
JSX 不是标准 JavaScript，需通过 Babel 编译为React.createElement调用。
示例 JSX：
jsx
const element = <h1 className="title">Hello, world!</h1>;
编译后：
jsx
const element = React.createElement(
  'h1',
  { className: 'title' },
  'Hello, world!'
);
React.createElement 返回值
返回一个轻量级 JavaScript 对象（虚拟 DOM），包含：
type：标签名（如'h1'）或组件类型。
props：属性对象（如{ className: 'title' }）。
children：子元素数组。
虚拟 DOM 结构示例：
javascript
{
  type: 'h1',
  props: {
    className: 'title',
    children: 'Hello, world!'
  },
  $$typeof: Symbol.for('react.element')
}
2. 虚拟 DOM 树的构建
组件渲染时，render方法返回的 JSX 被编译为虚拟 DOM 对象。
多层级 JSX 结构会递归生成嵌套的虚拟 DOM 树。
示例组件：
jsx
function App() {
  return (
    <div className="container">
      <h1>Hello</h1>
      <p>World</p>
    </div>
  );
}
对应的虚拟 DOM 树：
javascript
{
  type: 'div',
  props: {
    className: 'container',
    children: [
      { type: 'h1', props: { children: 'Hello' } },
      { type: 'p', props: { children: 'World' } }
    ]
  }
}
3. 首次渲染：虚拟 DOM 转换为真实 DOM
ReactDOM.render()
将根组件的虚拟 DOM 转换为真实 DOM 并挂载到容器节点。
jsx
ReactDOM.render(<App />, document.getElementById('root'));
调和（Reconciliation）过程
创建根 DOM 节点：根据虚拟 DOM 的type创建对应的真实 DOM 节点（如div、h1）。
设置属性：将虚拟 DOM 的props映射到真实 DOM 的属性（如className → class）。
递归处理子节点：对每个子虚拟 DOM 重复上述步骤。
挂载到容器：将完整的 DOM 树插入到目标容器（如#root）。
4. 更新阶段：Diff 算法与 DOM 更新
触发更新
组件状态（state或props）变化时，重新调用render生成新的虚拟 DOM。
Diff 算法
React 比较新旧虚拟 DOM 的差异，找出最小变更集：
不同类型元素：直接替换整个节点及其子树。
相同类型元素：仅更新变更的属性（如className、style）。
列表元素：通过key识别同层级的元素，优化移动 / 删除操作。
批处理与优化
React 将多次状态更新合并为一次 DOM 操作，减少重排 / 重绘：
jsx
// 仅触发一次DOM更新
this.setState({ count: 1 });
this.setState({ count: 2 });

5. 关键优化点
虚拟 DOM 缓存
组件实例会缓存上次渲染的虚拟 DOM，用于与新虚拟 DOM 比较。
shouldComponentUpdate
类组件可通过此生命周期方法阻止不必要的渲染：
jsx
shouldComponentUpdate(nextProps, nextState) {
  return nextState.count !== this.state.count;
}

React.memo
函数组件使用React.memo浅比较 props，避免重复渲染：
jsx
const MyComponent = React.memo(({ count }) => {
  return <div>{count}</div>;
});

6. 常见面试题扩展
为什么需要虚拟 DOM？
避免直接操作真实 DOM 的高成本，通过 Diff 算法最小化 DOM 变更。
实现跨平台渲染（如 React Native）。
Diff 算法的复杂度是多少？
理论上比较两棵树的时间复杂度为 O (n³)，但 React 通过以下假设优化到 O (n)：
不同类型的元素生成不同的树。
开发者通过key标识稳定的子元素。
key 的作用是什么？
帮助 React 识别哪些元素发生了变化，提高列表渲染效率。
推荐使用稳定、唯一的标识（如数据库 ID），避免使用索引（可能导致重排序时的状态混乱）。
React 18 的并发渲染如何影响这一过程？
并发渲染允许 React 中断、恢复或重新执行渲染，可能导致render方法被多次调用。
但最终只有一次渲染结果会被应用到真实 DOM，确保用户看到的是一致的 UI。
7. 总结
JSX 到真实 DOM 的转换过程可概括为：
编译：JSX → React.createElement → 虚拟 DOM 对象。
首次渲染：虚拟 DOM → 真实 DOM（递归创建节点并挂载）。
更新阶段：新状态触发重新渲染 → 生成新虚拟 DOM → Diff 算法计算差异 → 仅更新变化的真实 DOM。
理解这一过程有助于优化 React 应用性能（如合理使用key、避免不必要的渲染），也是面试中的高频考点。`
              }
            ]
          },
          {
            question: `20.说说对Fiber架构的理解？解决了什么问题？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Fiber 是 React 16 + 引入的协调引擎（Reconciler），它彻底重构了 React 的渲染机制，核心目标是实现增量渲染（Incremental Rendering）和优先级调度（Priority Scheduling）。以下是详细解析：
1. 核心概念
Fiber 的本质
一种数据结构：每个 React 元素对应一个 Fiber 节点，包含优先级、副作用等元数据。
一种执行机制：将渲染任务拆分为多个 “工作单元”（Work Unit），可暂停、恢复和优先级排序。
与旧协调器的对比
Stack Reconciler（旧）：
递归渲染，无法中断（渲染过程中浏览器无法响应其他任务）。
大型应用可能导致长时间卡顿（超过 16ms 会出现视觉卡顿）。
Fiber Reconciler（新）：
分阶段渲染：将渲染任务拆分为多个小单元，穿插在浏览器的空闲时间执行。
可中断、可恢复，支持优先级调度。
2. 核心特性
增量渲染（Incremental Rendering）
将渲染任务拆分为多个小单元，每次只执行一部分，剩余的推迟到下一帧：
javascript
// 示意：Fiber将大型渲染任务分片
function workLoop(hasTimeRemaining) {
  while (nextUnitOfWork && (hasTimeRemaining || shouldYield())) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  requestIdleCallback(workLoop); // 浏览器空闲时继续执行
}

优先级调度
为不同类型的更新分配优先级：
高优先级：用户交互（如点击、输入）、动画。
低优先级：数据获取、不影响 UI 的后台更新。
双缓存 Fiber 树
维护两棵 Fiber 树：
current：当前显示在屏幕上的树。
workInProgress：正在构建的新树（完成后替换 current）。
通过指针切换实现高效更新，避免频繁创建新对象。
3. 解决的核心问题
1. 长时间卡顿问题
旧架构：大型组件树的渲染可能阻塞主线程，导致页面失去响应。
Fiber：通过增量渲染，将渲染任务分散到多个帧中，确保主线程始终可响应（如滚动、输入）。
2. 无法中断的渲染
旧架构：一旦开始渲染，无法中途暂停。
Fiber：支持渲染过程中暂停、恢复和调整优先级，例如：
用户正在输入时，优先处理输入事件，暂停低优先级的渲染。
动画需要高帧率，优先完成动画相关的渲染。
3. 更灵活的生命周期管理
新增 “可中断” 生命周期：如getDerivedStateFromProps、getSnapshotBeforeUpdate。
废弃不安全的生命周期：如componentWillMount、componentWillReceiveProps（因可能在异步渲染中被多次调用）。
4. 与 React 18 并发特性的关系
并发渲染（Concurrent Rendering）
Fiber 是并发渲染的基础，允许 React：
同时准备多个版本的 UI（如预加载新路由的组件）。
根据用户交互动态调整渲染优先级。
新 API 支持
useTransition：标记低优先级更新，不阻塞用户交互：
jsx
const [isPending, startTransition] = useTransition();

const handleClick = () => {
  startTransition(() => {
    setCount(count + 1); // 低优先级更新
  });
};

useDeferredValue：延迟计算高成本值，避免阻塞渲染：
jsx
const deferredValue = useDeferredValue(value, { timeoutMs: 500 });

5. 常见面试题扩展
Fiber 如何实现增量渲染？
将渲染任务拆分为 Fiber 节点，每个节点作为一个工作单元。
使用requestIdleCallback（浏览器空闲时执行）或requestAnimationFrame（动画帧开始时执行）。
Fiber 架构对生命周期有什么影响？
引入 “render 阶段” 和 “commit 阶段”：
render 阶段：可中断，包括componentWillMount、componentWillReceiveProps等（已废弃）。
commit 阶段：不可中断，包括componentDidMount、componentDidUpdate等。
Fiber 与虚拟 DOM 的关系？
虚拟 DOM：描述 UI 的轻量级 JavaScript 对象。
Fiber：扩展了虚拟 DOM，增加了优先级、副作用等元数据，用于实现增量渲染和调度。
Fiber 如何提高应用性能？
避免长时间阻塞主线程，提高交互响应性。
通过优先级调度，确保关键 UI（如动画、输入）优先渲染。
减少不必要的 DOM 操作（双缓存机制）。
6. 总结
Fiber 架构是 React 自发布以来最重大的内部变革，其核心价值在于：
解决渲染阻塞问题：通过增量渲染和优先级调度，提升用户体验。
支持并发特性：为 React 18 的并发渲染、过渡效果等新特性奠定基础。
优化生命周期管理：明确区分可中断和不可中断的生命周期阶段。
理解 Fiber 有助于深入掌握 React 的工作原理，也是面试中区分候选人技术深度的关键考点。`
              }
            ]
          },
          {
            question: `21. React中的key有什么作用?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，key是用于帮助识别哪些元素发生变化的特殊属性，主要作用是优化列表渲染效率。以下是详细解析：
1. 核心作用
帮助 React 识别元素变化
在列表渲染时，React 通过key判断哪些元素被添加、删除或移动。
示例场景：
jsx
// 初始列表
<ul>
  <li key="1">Apple</li>
  <li key="2">Banana</li>
</ul>

// 添加一项后
<ul>
  <li key="1">Apple</li>
  <li key="2">Banana</li>
  <li key="3">Cherry</li> // React通过key识别新增项
</ul>

2. 原理：Diff 算法与 key 的关系
无 key 的默认行为
React 默认使用元素索引（index）作为 key，导致：
插入 / 删除元素时可能出现状态混乱（如输入框内容错位）。
性能低效（大量元素被重新创建而非复用）。
有 key 的优化
稳定的 key 让 React：
识别元素身份：通过 key 匹配新旧元素，仅更新真正变化的元素。
复用 DOM 节点：相同 key 的元素即使位置变化，也会复用已有 DOM。
示例对比：
jsx
// 反例：使用索引作为key（不稳定）
const items = ['A', 'B', 'C'];
items.map((item, index) => <li key={index}>{item}</li>);

// 正例：使用唯一ID作为key（稳定）
const items = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
items.map((item) => <li key={item.id}>{item.name}</li>);
3. 最佳实践
使用稳定的唯一标识
优先使用数据的唯一 ID（如数据库 ID）：
jsx
users.map(user => <User key={user.id} {...user} />);

避免使用索引作为 key
索引会随元素位置变化而变化，导致状态问题：
jsx
// 错误：索引作为key
items.map((item, index) => <Item key={index} {...item} />);

局部列表可使用组合 key
当数据无唯一 ID 时，组合多个属性生成唯一 key：
jsx
posts.map(post => (
  <Comment key={‘￥{ post.title } - ￥{ post.date }’} {...post} />
));

4. 常见面试题扩展
为什么不推荐使用索引作为 key？
插入 / 删除元素时，索引会变化，导致：
React 误判元素身份（如将新元素误认为旧元素）。
组件状态丢失（如输入框内容、动画状态）。
key 必须全局唯一吗？
不需要。只需在同级兄弟元素中唯一即可。
key 对性能的影响？
正确的 key 可大幅减少 DOM 操作：
无 key 时，插入元素可能导致其后所有元素被重新创建。
有 key 时，仅插入新元素，其他元素保持不变。
动态生成的 key（如 Math.random ()）会有什么问题？
每次渲染时 key 都不同，导致：
所有元素被视为新元素，强制重新创建。
性能严重下降，状态丢失。
5. 总结
核心作用：帮助 React 高效识别列表中的变化元素，减少不必要的 DOM 操作。
最佳实践：使用稳定、唯一的标识（如数据 ID），避免索引。
性能优化：正确的 key 是列表渲染性能的关键，尤其在大数据量场景下。
理解 key 的工作原理，能避免 React 应用中常见的状态异常和性能问题。`
              }
            ]
          },
          {
            question: `22.说说Reactdiff的原理是什么?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React Diff 算法（调和算法）是 React 实现高效更新的核心机制，其目标是通过比较新旧虚拟 DOM 的差异，最小化真实 DOM 的操作。以下是其原理的详细解析：
1. 核心设计思路
问题复杂度
理论上，比较两棵树的差异需要 O (n³) 的时间复杂度（n 为节点数），无法用于实际应用。
React 的简化假设
同层比较：只比较同一层级的节点，跨层级移动视为删除和新建。
不同类型元素：不同类型的元素会生成不同的树。
可通过 key 标识元素：通过key稳定识别列表中的元素。
2. 算法实现细节
1. 元素类型比较
相同类型元素：
保留 DOM 节点，仅更新属性（如className、style）。
递归处理子节点。
不同类型元素：
销毁旧节点及其子树。
创建新节点及其子树。
示例：
jsx
// 旧元素
<div className="old">...</div>

// 新元素（类型相同）
<div className="new">...</div>
// 仅更新className，保留DOM节点

// 新元素（类型不同）
<span className="new">...</span>
// 销毁div，创建span
2. 列表比较与 key 的作用
无 key 时：默认按索引比较，插入 / 删除元素可能导致后续元素全部重新创建。
有 key 时：通过 key 匹配新旧元素，仅操作真正变化的元素。
示例：无 key 导致的问题
jsx
// 初始列表（索引作为key）
[<li key="0">A</li>, <li key="1">B</li>]

// 插入新元素后
[<li key="0">C</li>, <li key="1">A</li>, <li key="2">B</li>]
// React认为索引0的元素从A变为C，索引1的元素从B变为A，导致所有元素重新渲染
示例：使用稳定 key 的优化
jsx
// 初始列表（ID作为key）
[<li key="a">A</li>, <li key="b">B</li>]

// 插入新元素后
[<li key="c">C</li>, <li key="a">A</li>, <li key="b">B</li>]
// React通过key识别到A和B未变化，仅插入C
3. 子节点比较策略
策略一：头对头比较
比较新旧列表的头部元素，适用于常见的添加操作。
策略二：尾对尾比较
比较新旧列表的尾部元素，适用于常见的删除操作。
策略三：key 映射表
构建旧列表的key → index映射表，快速查找元素：
javascript
// 旧列表的映射表
{ key1: 0, key2: 1, key3: 2 }

新列表通过 key 快速定位旧元素，优化移动操作。
3. 性能优化点
1. 减少跨层级移动
React 对跨层级移动的处理效率低（视为删除和新建），应避免：
jsx
// 不推荐：从A移到B
<A>
  <Child key="child" />
</A>

<B>
  <Child key="child" /> // React会销毁原Child，创建新Child
</B>

2. 避免频繁创建新组件类型
不同类型的组件会被完全替换：
jsx
// 每次渲染都会创建新的FunctionComponent，导致重新渲染
const App = () => {
  const FunctionComponent = () => <div>Hello</div>;
  return <FunctionComponent />;
};

3. 合理使用 key
使用稳定、唯一的 key（如数据 ID），避免索引作为 key。
4. 与 Fiber 架构的关系
Fiber 对 Diff 的扩展
增量计算：将 Diff 过程拆分为多个小任务，可中断、恢复。
优先级调度：为不同类型的更新分配优先级，高优先级任务可插队。
副作用标记
Fiber 节点通过flags标记需要执行的 DOM 操作（如Placement、Update、Deletion），最终批量执行。
5. 常见面试题扩展
Diff 算法的时间复杂度是多少？
理论上为 O (n³)，但 React 通过三个简化假设将其优化到 O (n)。
key 的作用是什么？
帮助 React 识别哪些元素发生了变化，优化列表渲染。
推荐使用稳定、唯一的 ID，避免使用索引。
React 如何处理组件更新？
组件类型相同时，保留实例，仅更新 props 和 state。
组件类型不同时，销毁旧实例，创建新实例。
React 18 的并发渲染如何影响 Diff？
并发渲染可能导致 Diff 过程被中断，需确保算法的幂等性（多次执行结果相同）。
6. 总结
React Diff 算法通过三个核心假设（同层比较、类型差异、key）实现了高效的虚拟 DOM 比较，其关键点包括：
分层比较：只比较同一层级的节点。
key 的重要性：稳定的 key 是列表渲染性能的关键。
Fiber 扩展：支持增量计算和优先级调度，提升用户体验。
理解 Diff 算法有助于编写高性能的 React 代码，避免常见的性能陷阱。`
              }
            ]
          },
          {
            question: `23.说说对ReactHooks的理解?解决了什么问题?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React Hooks 是 React 16.8 引入的新特性，允许在不编写 class 的情况下使用 state 和其他 React 特性。它解决了 React 长期存在的状态逻辑复用难、组件复杂度过高和class 的学习成本等问题。以下是详细解析：
1. 核心概念
定义
纯函数：不依赖 this，不继承 React.Component。
可组合：多个 Hook 可在一个组件中自由组合使用。
常用 Hooks
基础 Hook：
useState：管理组件状态。
useEffect：处理副作用（如 API 请求、订阅）。
useContext：获取 Context 值。
额外 Hook：
useReducer：复杂状态管理（替代 useState）。
useCallback：缓存函数引用。
useMemo：缓存计算结果。
useRef：保存可变值或引用 DOM。
2. 解决的核心问题
1. 状态逻辑复用困难
问题：HOC 和 Render Props 导致组件嵌套过深（“嵌套地狱”）。
解决方案：自定义 Hook 可复用状态逻辑，且不增加组件层级。
示例：自定义 useFetch Hook
jsx
// 复用API请求逻辑
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(data => setData(data))
      .finally(() => setLoading(false));
  }, [url]);
  
  return { data, loading };
};

// 在组件中使用
const Posts = () => {
  const { data, loading } = useFetch('/api/posts');
  return loading ? <Spinner /> : <List data={data} />;
};
2. 组件复杂度过高
问题：生命周期方法中混杂不相关逻辑（如 API 请求、清理订阅）。
解决方案：useEffect按逻辑关注点拆分代码。
示例：拆分不同副作用
jsx
const TimerComponent = () => {
  // 逻辑1：设置定时器
  useEffect(() => {
    const timer = setInterval(() => { /* ... */ }, 1000);
    return () => clearInterval(timer); // 清理定时器
  }, []);
  
  // 逻辑2：订阅事件
  useEffect(() => {
    const handler = () => { /* ... */ };
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  
  return <div>Timer</div>;
};
3. Class 的学习成本和缺陷
问题：
this指向问题（需绑定方法）。
生命周期方法逻辑分散。
代码结构不清晰（状态逻辑与 UI 逻辑混杂）。
解决方案：Hooks 使用纯函数，无 this 问题，逻辑更集中。
3. 设计原则
1. 仅在顶层调用 Hook
不要在循环、条件或嵌套函数中调用 Hook，确保 Hook 调用顺序稳定。
2. 仅在 React 函数中调用 Hook
不要在普通 JavaScript 函数中调用 Hook。
3. 依赖数组的精确控制
useEffect和useMemo的依赖数组需包含所有依赖项，避免闭包陷阱。
4. 与 Class 的对比
特性	Class	Hooks
状态管理	通过 this.state 和 this.setState	通过 useState 或 useReducer
副作用处理	在 componentDidMount/Update 中处理	在 useEffect 中统一处理
逻辑复用	HOC 或 Render Props（嵌套层级深）	自定义 Hook（无额外层级）
this 指向	需要 bind 或箭头函数	无 this 问题
代码结构	按生命周期组织（逻辑分散）	按逻辑关注点组织（逻辑内聚）
5. 常见面试题扩展
为什么 Hook 不能在条件语句中调用？
React 依赖 Hook 调用顺序识别状态。条件调用会破坏顺序，导致状态混乱。
useEffect 与生命周期的关系？
useEffect合并了componentDidMount、componentDidUpdate和componentWillUnmount的功能。
通过返回清理函数模拟componentWillUnmount。
如何避免 useEffect 的无限循环？
确保依赖数组包含所有外部依赖。
对不需要触发 effect 的依赖使用useCallback或useMemo缓存。
useState 是同步还是异步的？
批量更新：在 React 事件处理中异步（合并多次更新）。
非批量更新：在 setTimeout、Promise 等非 React 回调中同步。
6. 总结
React Hooks 通过纯函数和可复用的状态逻辑，解决了 class 组件的核心痛点：
逻辑复用：自定义 Hook 使状态逻辑可复用，无嵌套地狱。
代码组织：按逻辑关注点拆分副作用，提升可维护性。
学习成本：消除 this 指向、生命周期等复杂概念。
理解 Hooks 的设计理念和使用规则，是编写现代 React 应用的关键。`
              }
            ]
          },
          {
            question: `24.说说你是如何提高组件的渲染效率的？在React中如何避免不必要的render?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，避免不必要的渲染是优化性能的关键。以下是提高组件渲染效率的核心策略，结合原理和实践案例说明：
一、核心优化原理
1. React 渲染流程
触发更新：state或props变化。
执行 render：生成新虚拟 DOM。
Diff 算法：比较新旧虚拟 DOM。
DOM 更新：仅修改必要的真实 DOM 节点。
2. 优化目标
减少 render 次数：避免执行不必要的render方法。
减少 Diff 范围：通过结构稳定和key优化，缩小比较范围。
减少 DOM 操作：通过精确的 Diff 结果，最小化真实 DOM 修改。
二、避免不必要 render 的具体方法
1. 类组件：使用 shouldComponentUpdate
作用：手动控制组件是否需要更新。
示例：
jsx
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.count !== this.props.count; // 仅在count变化时更新
  }
  render() {
    return <div>{this.props.count}</div>;
  }
}

2. 函数组件：使用 React.memo
作用：高阶组件，浅比较 props，阻止重复渲染。
示例：
jsx
const MyComponent = React.memo(({ count }) => {
  return <div>{count}</div>;
});

3. 避免内联函数 / 对象
问题：每次渲染生成新引用，导致React.memo失效。
解决方案：
useCallback：缓存函数引用。
jsx
const handleClick = useCallback(() => {
  // 处理逻辑
}, []); // 空依赖数组，仅创建一次

useMemo：缓存对象 / 数组。
jsx
const styles = useMemo(() => ({ color: 'red' }), []);

4. 拆分组件：粒度控制渲染范围
原则：将易变部分与稳定部分分离。
示例：
jsx
// 错误：整个组件重新渲染
const Parent = ({ user }) => {
  return (
    <div>
      <StableHeader /> // 稳定部分
      <UserInfo user={user} /> // 易变部分
    </div>
  );
};

// 正确：拆分后仅UserInfo重新渲染
const Parent = ({ user }) => {
  return (
    <div>
      <StableHeader />
      <UserInfo user={user} /> // 单独组件，可被React.memo优化
    </div>
  );
};

5. 使用不可变数据（Immutable Data）
问题：直接修改对象 / 数组导致引用不变，React 无法检测变化。
解决方案：
使用展开语法（Spread）创建新对象 / 数组。
jsx
// 错误：直接修改
this.setState({ items: this.state.items.push(newItem) });

// 正确：创建新数组
this.setState({ items: [...this.state.items, newItem] });

使用 Immer 库简化不可变操作。
jsx
import produce from 'immer';

const nextState = produce(currentState, draft => {
  draft.items.push(newItem); // 可直接修改，produce自动生成新对象
});

6. 避免在 render 中创建新组件
问题：每次渲染生成新组件类型，导致强制重新渲染。
示例：
jsx
// 错误：每次render创建新组件
const Parent = () => {
  const Child = () => <div>Child</div>; // 不要在render中定义组件
  return <Child />;
};

// 正确：组件定义在外部
const Child = () => <div>Child</div>;

const Parent = () => {
  return <Child />;
};

7. 使用 React.lazy 和 Suspense 延迟加载
场景：非关键组件（如模态框、详情页）。
示例：
jsx
const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => {
  return (
    <div>
      <React.Suspense fallback={<Spinner />}>
        <LazyComponent />
      </React.Suspense>
    </div>
  );
};

8. 合理使用 useMemo 缓存计算结果
场景：避免昂贵计算在每次渲染时重复执行。
示例：
jsx
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // 仅当items变化时重新计算

三、性能监控工具
1. React DevTools Profiler
作用：记录渲染时间，标记不必要的渲染。
使用步骤：
在 Chrome DevTools 中打开 Profiler 面板。
点击 "Record"，操作应用。
分析火焰图，查找频繁渲染的组件。
2. why-did-you-render
作用：追踪组件渲染原因，在控制台输出详细日志。
安装：
bash
npm install @welldone-software/why-did-you-render

配置：
jsx
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

四、常见面试题扩展
React.memo 与 shouldComponentUpdate 的区别？
React.memo用于函数组件，仅浅比较 props。
shouldComponentUpdate用于类组件，可自定义比较逻辑，同时比较 props 和 state。
useMemo 与 useCallback 的区别？
useMemo缓存计算结果（值）。
useCallback缓存函数引用。
如何优化大型列表渲染？
使用虚拟列表（如 react-window、react-virtualized）。
为列表项提供稳定的key。
使用React.memo包裹列表项组件。
React 18 的自动批处理（Automatic Batching）如何影响渲染？
在 React 18 中，所有更新（包括 Promise、setTimeout）都默认批处理，减少渲染次数。
示例：
jsx
// React 18中，即使在setTimeout中也会批处理
setTimeout(() => {
  setCount(c => c + 1);
  setCount(c => c + 1); // 只触发一次渲染
}, 0);

五、总结
提高 React 组件渲染效率的核心思路是：减少不必要的渲染，缩小 Diff 范围，优化 DOM 操作。具体策略包括：
使用React.memo和shouldComponentUpdate：阻止重复渲染。
避免内联函数 / 对象：通过useCallback和useMemo缓存引用。
拆分组件：控制渲染粒度。
不可变数据：确保状态变化被正确检测。
延迟加载：使用React.lazy和Suspense处理非关键组件。
性能监控：利用工具定位瓶颈。
通过以上方法，可显著提升 React 应用的响应速度和用户体验。`
              }
            ]
          },
          {
            question: `25.说说React性能优化的手段有哪些?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 中，性能优化是构建高效应用的关键。以下从渲染、内存、代码组织和构建工具四个维度，系统梳理 React 性能优化的核心手段：
一、渲染优化
1. 避免不必要的渲染
React.memo（函数组件）：
jsx
const MyComponent = React.memo(({ data }) => {
  return <div>{data}</div>;
});

shouldComponentUpdate（类组件）：
jsx
shouldComponentUpdate(nextProps, nextState) {
  return nextProps.id !== this.props.id;
}

PureComponent：自动浅比较 props 和 state。
2. 不可变数据模式
错误示例：
jsx
this.state.items.push(newItem); // 直接修改原数组

正确示例：
jsx
this.setState({ items: [...this.state.items, newItem] }); // 创建新数组

Immer 库：简化不可变操作：
jsx
import produce from 'immer';
const nextState = produce(currentState, draft => {
  draft.items.push(newItem); // 可直接修改，produce自动生成新对象
});

3. 虚拟列表（Virtualization）
场景：大数据列表（如 1000 + 项）。
工具：react-window或react-virtualized。
示例：
jsx
import { FixedSizeList } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>{items[index]}</div>
);

const App = () => (
  <FixedSizeList
    height={600}
    width={300}
    itemSize={35}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

二、内存优化
1. 避免内联函数 / 对象
问题：每次渲染生成新引用，触发不必要更新。
解决方案：
jsx
// 使用useCallback缓存函数
const handleClick = useCallback(() => {
  console.log('Clicked');
}, []); // 空依赖数组，仅创建一次

// 使用useMemo缓存对象
const styles = useMemo(() => ({ color: 'red' }), []);

2. 资源清理
useEffect 的清理函数：
jsx
useEffect(() => {
  const subscription = api.subscribe();
  return () => subscription.unsubscribe(); // 组件卸载时清理
}, []);

取消异步操作：
jsx
useEffect(() => {
  let isMounted = true;
  fetchData().then(data => {
    if (isMounted) setData(data); // 避免组件卸载后更新状态
  });
  return () => (isMounted = false);
}, []);

三、代码组织优化
1. 组件拆分与粒度控制
原则：将易变部分与稳定部分分离。
示例：
jsx
// 错误：整个组件重新渲染
const Parent = ({ user }) => {
  return (
    <div>
      <StableHeader /> // 稳定部分
      <UserInfo user={user} /> // 易变部分
    </div>
  );
};

// 正确：拆分后仅UserInfo重新渲染
const Parent = ({ user }) => {
  return (
    <div>
      <StableHeader />
      <React.memo(UserInfo) user={user} /> // 单独组件，可被React.memo优化
    </div>
  );
};

2. 懒加载（Code Splitting）
React.lazy 与 Suspense：
jsx
const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => {
  return (
    <React.Suspense fallback={<Spinner />}>
      <LazyComponent />
    </React.Suspense>
  );
};

路由懒加载：
jsx
const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);

四、构建与工具优化
1. Tree Shaking
条件：使用 ES 模块（import/export），避免默认导出整个对象。
错误示例：
jsx
// utils.js
export const a = () => {};
export const b = () => {};

// 导入整个对象，无法Tree Shaking
import * as utils from './utils';

正确示例：
jsx
// 仅导入需要的函数
import { a } from './utils';

2. 生产环境优化
移除开发代码：
jsx
if (process.env.NODE_ENV !== 'production') {
  console.log('开发环境日志');
}

压缩与混淆：
bash
# 使用Create React App默认配置
npm run build

3. 使用 CDN 加载第三方库
示例：在 index.html 中引入 React：
html
预览
《script src="https://cdn.tailwindcss.com">《/script>
《script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js">《/script>
《script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js">《/script>

五、高级优化技术
1. React 18 的并发特性
useTransition：标记低优先级更新：
jsx
const [isPending, startTransition] = useTransition();

const handleSearch = (query) => {
startTransition(() => {
setSearchQuery(query); // 低优先级更新，不阻塞UI
});
};

useDeferredValue：延迟计算高成本值：
jsx
const deferredValue = useDeferredValue(value, { timeoutMs: 500 });

2. 服务端渲染（SSR）/ 静态站点生成（SSG）
工具：Next.js、Gatsby。
优势：首屏加载更快，SEO 友好。
六、性能监控工具
1. React DevTools Profiler
作用：分析组件渲染时间，识别性能瓶颈。
使用步骤：
打开 Chrome DevTools → Profiler。
点击 "Record"，操作应用。
分析火焰图，关注耗时较长的组件。
2. Lighthouse
指标：性能、可访问性、最佳实践等。
运行方式：
bash
npx lighthouse https://yourwebsite.com

七、常见面试题扩展
React.memo 与 PureComponent 的区别？
React.memo用于函数组件，仅浅比较 props。
PureComponent用于类组件，浅比较 props 和 state。
如何优化大型表单？
使用useReducer集中管理表单状态。
实现shouldComponentUpdate或使用React.memo阻止不必要的渲染。
使用虚拟滚动处理大量表单字段。
useMemo 与 useCallback 的适用场景？
useMemo：缓存计算结果（如复杂数据处理）。
useCallback：缓存函数引用，避免子组件重复渲染。
CSS-in-JS 对性能的影响？
运行时 CSS-in-JS（如 styled-components）可能有轻微性能开销。
编译时 CSS-in-JS（如 Linaria、Tailwind CSS）性能更优。
八、总结
React 性能优化的核心策略可概括为：
减少渲染次数：通过React.memo、不可变数据、合理组件拆分。
优化内存使用：避免内联函数、及时清理资源。
降低包体积：使用懒加载、Tree Shaking、CDN。
利用现代特性：React 18 并发渲染、SSR/SSG。
持续监控：使用 Profiler、Lighthouse 等工具追踪性能。
通过系统化的优化手段，可显著提升 React 应用的响应速度和用户体验。`
              }
            ]
          },
          {
            question: `26.说说你对ReactRouter的理解?常用的Router组件有哪些?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React Router 是 React 官方的路由库，用于实现单页面应用（SPA）的路由功能。它通过组件化的方式管理路由逻辑，支持浏览器端、服务器端和原生应用。以下是详细解析：
一、核心概念
1. 声明式路由
路由配置通过 React 组件定义，而非命令式 API。
示例：
jsx
<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="*" element={<NotFound />} /> {/* 404路由 */}
  </Routes>
</BrowserRouter>

2. 路由匹配规则
精确匹配：使用exact属性（v5）或严格路径（v6）。
动态参数：使用:定义参数（如/user/:id）。
嵌套路由：父路由通过Outlet组件渲染子路由。
二、常用 Router 组件（以 v6 为例）
1. 路由器组件
BrowserRouter：使用 HTML5 History API（推荐）。
jsx
import { BrowserRouter } from 'react-router-dom';

const App = () => (
  <BrowserRouter>
    <AppLayout />
  </BrowserRouter>
);

HashRouter：使用 URL 哈希（#），适用于不支持 History API 的环境。
2. 路由匹配组件
Routes：包裹所有 Route 的容器，匹配第一个符合条件的路由。
Route：定义路径和对应组件。
jsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/users" element={<Users />}>
    <Route path=":id" element={<UserDetail />} /> {/* 嵌套路由 */}
  </Route>
</Routes>

3. 导航组件
Link：声明式导航（生成 a 标签）。
jsx
<Link to="/about">About</Link>

NavLink：Link 的扩展，支持激活状态样式。
jsx
<NavLink to="/home" className={({ isActive }) => isActive ? 'active' : ''}>
  Home
</NavLink>

useNavigate：函数式导航（替代 v5 的withRouter和useHistory）。
jsx
const navigate = useNavigate();

const handleClick = () => {
  navigate('/dashboard'); // 跳转
  navigate(-1); // 返回上一页
};

4. 路由信息钩子
useParams：获取动态路由参数。
jsx
// 路径: /users/:id
const { id } = useParams();

useLocation：获取当前 URL 信息。
jsx
const location = useLocation();
console.log(location.pathname); // 输出当前路径

useRouteMatch（v5）/ useMatch（v6）：手动匹配路由。
三、高级特性
1. 嵌套路由与 Outlet
父组件：
jsx
const Users = () => (
  <div>
    <h1>Users</h1>
    <Outlet /> {/* 子路由渲染位置 */}
  </div>
);

路由配置：
jsx
<Route path="/users" element={<Users />}>
  <Route path=":id" element={<UserDetail />} />
</Route>

2. 动态加载（懒加载）
结合 React.lazy 和 Suspense 实现组件懒加载：
jsx
const Home = React.lazy(() => import('./pages/Home'));

<Routes>
  <Route 
    path="/" 
    element={
      <React.Suspense fallback={<Spinner />}>
        <Home />
      </React.Suspense>
    } 
  />
</Routes>

3. 导航守卫（权限控制）
示例：验证用户登录状态：
jsx
const PrivateRoute = ({ children }) => {
  const isAuthenticated = useAuth();
  return isAuthenticated ? children : <Navigate to="/login" />;
};

<Route 
  path="/dashboard" 
  element={
    <PrivateRoute>
      <Dashboard />
    </PrivateRoute>
  } 
/>

4. 错误处理
使用errorElement捕获路由错误：
jsx
<Routes errorElement={<ErrorBoundary />}>
  <Route path="/" element={<Home />} />
</Routes>

四、版本差异（v5 vs v6）
特性	React Router v5	React Router v6
路由配置	使用Switch包裹Route	使用Routes和Route
精确匹配	需显式设置exact属性	严格路径匹配（如path="/home"）
嵌套路由	通过children属性渲染子路由	通过Outlet组件渲染子路由
动态路由参数	通过match.params获取	通过useParams钩子获取
导航方式	使用withRouter高阶组件或useHistory	使用useNavigate钩子
重定向	使用Redirect组件	使用Navigate组件
五、常见面试题扩展
React Router 如何实现 SPA 路由？
通过监听浏览器 URL 变化（History API 或 hashchange 事件）。
根据当前 URL 匹配对应的 React 组件。
使用虚拟 DOM 更新页面，而非完整刷新。
Link 和 a 标签的区别？
a 标签：触发浏览器刷新，加载新页面。
Link：阻止默认行为，使用 History API 更新 URL，触发 React 组件更新。
如何实现路由懒加载？
使用 React.lazy 动态导入组件：
jsx
const Home = React.lazy(() => import('./Home'));

配合 Suspense 提供加载状态：
jsx
<React.Suspense fallback={<Spinner />}>
  <Home />
</React.Suspense>

React Router v6 的主要变化？
更简洁的 API（如useNavigate替代useHistory）。
严格路径匹配，无需exact。
嵌套路由通过Outlet实现。
新增Navigate组件用于重定向。
六、总结
React Router 是构建 React SPA 的核心工具，其核心优势在于：
组件化设计：路由配置即代码，易于维护。
灵活的导航方式：支持声明式（Link）和命令式（useNavigate）导航。
丰富的钩子：提供useParams、useLocation等获取路由信息。
高级特性：嵌套路由、懒加载、导航守卫等满足复杂场景。
理解 React Router 的工作原理和组件使用，是开发大型 React 应用的必备技能。`
              }
            ]
          },
          {
            question: `27.说说ReactRouter有几种模式?实现原理?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React Router 支持两种主要路由模式：Browser Router（浏览器路由）和Hash Router（哈希路由），它们基于不同的浏览器 API 实现单页面应用（SPA）的路由功能。以下是详细解析：
一、Browser Router（浏览器路由）
1. 核心原理
基于 HTML5 History API：使用history.pushState和history.replaceState管理 URL，不触发页面刷新。
URL 格式：标准路径（如https://example.com/users/123）。
2. 实现机制
监听 URL 变化：通过popstate事件监听浏览器后退 / 前进按钮。
路由匹配：URL 变化时，React Router 重新匹配 Route 组件。
组件渲染：根据匹配结果渲染对应组件。
3. 代码示例
jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const App = () => (
  <Router>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/users/:id" element={<User />} />
    </Routes>
  </Router>
);
4. 服务器配置要求
所有请求指向 index.html：由于 SPA 只有一个 HTML 文件，服务器需将所有路径请求（如/users/123）重定向到index.html。
示例（Express）：
javascript
const express = require('express');
const app = express();
const path = require('path');

app.use(express.static(path.join(__dirname, 'build')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

app.listen(3000);

二、Hash Router（哈希路由）
1. 核心原理
基于 URL 哈希（#）：使用window.location.hash管理路由，哈希变化不会触发页面刷新。
URL 格式：路径包含 #（如https://example.com/#/users/123）。
2. 实现机制
监听哈希变化：通过hashchange事件监听 URL 中 #后的内容变化。
路由匹配：React Router 解析哈希值，匹配对应 Route 组件。
组件渲染：根据匹配结果渲染组件。
3. 代码示例
jsx
import { HashRouter as Router, Routes, Route } from 'react-router-dom';

const App = () => (
  <Router>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/users/:id" element={<User />} />
    </Routes>
  </Router>
);
4. 服务器配置特点
无需特殊配置：哈希部分不会发送到服务器，服务器只需返回根页面（如index.html）。
三、两种模式的对比
特性	Browser Router	Hash Router
URL 格式	标准路径（如/users/123）	哈希路径（如/#/users/123）
历史记录	使用 History API，支持完整历史栈	使用哈希变化，历史记录可能受限
SEO 友好性	更好（标准 URL）	较差（搜索引擎可能忽略哈希部分）
服务器配置	需要配置所有路径指向 index.html	无需特殊配置
兼容性	现代浏览器（IE10+）	所有支持 JavaScript 的浏览器
四、其他路由模式
1. Memory Router（内存路由）
适用场景：测试环境或非浏览器环境（如 React Native）。
原理：将路由历史保存在内存中，不与浏览器 URL 交互。
示例：
jsx
import { MemoryRouter } from 'react-router-dom';

// 用于测试
render(
  <MemoryRouter initialEntries={['/']}>
    <App />
  </MemoryRouter>
);

2. Static Router（静态路由）
适用场景：服务器端渲染（SSR）。
原理：接收静态 URL 作为参数，不监听任何变化。
示例：
jsx
import { StaticRouter } from 'react-router-dom/server';

const html = renderToString(
  <StaticRouter location={req.url}>
    <App />
  </StaticRouter>
);

五、常见面试题扩展
Browser Router 和 Hash Router 的选择原则？
Browser Router：适合对 URL 美观性和 SEO 要求高的应用，需服务器支持。
Hash Router：适合无法配置服务器的场景（如静态网站）或兼容性要求高的应用。
如何实现路由切换时的动画效果？
使用 React Transition Group：
jsx
import { CSSTransition, TransitionGroup } from 'react-transition-group';

<TransitionGroup>
  <CSSTransition key={location.key} classNames="fade" timeout={300}>
    <Routes location={location}>
      <Route path="/" element={<Home />} />
    </Routes>
  </CSSTransition>
</TransitionGroup>

React Router 如何处理 404 页面？
使用通配符路由（v6）：
jsx
<Route path="*" element={<NotFound />} />

如何在组件外进行导航？
创建导航服务：
jsx
// history.js
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();

// App.js
import { Router } from 'react-router-dom';
<Router history={history}>
  <App />
</Router>

// 组件外导航
import { history } from './history';
history.push('/login');

六、总结
React Router 的不同模式基于浏览器不同 API 实现路由功能：
Browser Router：基于 History API，提供美观的 URL，但需服务器支持。
Hash Router：基于 URL 哈希，兼容性好，无需服务器配置。
Memory Router：用于测试或非浏览器环境。
Static Router：用于服务器端渲染。
选择合适的路由模式需根据项目需求（如 SEO、服务器配置、兼容性）综合考虑。理解其实现原理有助于解决路由相关的复杂问题。`
              }
            ]
          },
          {
            question: `28.你在React项目中是如何使用Redux的?项目结构是如何划分的?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `在 React 项目中使用 Redux 时，我遵循单向数据流和模块化的设计原则，结合现代 Redux Toolkit（RTK）简化开发流程。以下是我的实践方案：
一、Redux 使用流程
1. 项目初始化
bash
npx create-react-app my-app --template redux-typescript
2. 核心概念实现
定义 Slice（替代传统 reducer）：
tsx
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
}

const initialState: CounterState = { value: 0 };

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1; // RTK允许直接修改状态
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

创建 Store：
tsx
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

组件中使用：
tsx
// features/counter/Counter.tsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

const Counter = () => {
  const count = useSelector((state: RootState) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
};

二、项目结构划分
plaintext
src/
  app/
    store.ts          # 根store配置
    hooks.ts          # 自定义hook（如useAppDispatch）
  features/           # 按功能模块划分
    auth/             # 认证模块
      authSlice.ts    # 认证相关reducer/slice
      authService.ts  # 认证API服务
      AuthForm.tsx    # 认证表单组件
    posts/            # 文章模块
      postsSlice.ts   # 文章相关reducer/slice
      PostsList.tsx   # 文章列表组件
      PostForm.tsx    # 文章表单组件
  components/         # 通用组件（非Redux相关）
    Layout/
    Button/
    Input/
  utils/              # 工具函数
  api/                # API请求封装
  types/              # 全局类型定义
  index.tsx
三、异步操作处理
1. 使用 RTK Query
tsx
// features/posts/postsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
    }),
    addPost: builder.mutation({
      query: (post) => ({
        url: '/posts',
        method: 'POST',
        body: post,
      }),
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;
2. 组件中使用
tsx
const PostsList = () => {
  const { data, isLoading, error } = useGetPostsQuery();

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};
四、状态管理最佳实践
1. 规范化状态结构
ts
// 规范化后的状态结构
interface PostsState {
  entities: { [id: string]: Post }; // 对象形式存储实体
  ids: string[];                    // 有序ID列表
  loading: 'idle' | 'pending' | 'succeeded' | 'failed';
  error: string | null;
}
2. 不可变数据处理
使用 RTK 的createEntityAdapter：
tsx
import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';

const postsAdapter = createEntityAdapter<Post>();

const initialState: EntityState<Post> = postsAdapter.getInitialState({
  loading: 'idle',
});

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    addPost: postsAdapter.addOne,
    addManyPosts: postsAdapter.addMany,
    updatePost: postsAdapter.updateOne,
  },
});

五、测试策略
1. 测试 Slice
tsx
import { increment, decrement, counterSlice } from './counterSlice';

describe('counterSlice', () => {
  test('increment', () => {
    const state = counterSlice.reducer({ value: 0 }, increment());
    expect(state.value).toBe(1);
  });

  test('decrement', () => {
    const state = counterSlice.reducer({ value: 2 }, decrement());
    expect(state.value).toBe(1);
  });
});
2. 测试组件
tsx
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { store } from '../../app/store';
import Counter from './Counter';

test('renders counter value', () => {
  render(
    <Provider store={store}>
      <Counter />
    </Provider>
  );
  expect(screen.getByText('0')).toBeInTheDocument();
});
六、常见面试题扩展
Redux 的设计原则？
单一数据源（Single Source of Truth）。
状态只读（State is read-only）。
使用纯函数修改状态（Reducers are pure functions）。
Redux 与本地状态（useState）的适用场景？
Redux：跨组件共享状态、复杂状态逻辑、需要时间旅行调试。
useState：组件内部状态（如表单输入、显示控制）。
Redux 中间件的作用？
处理异步操作（如 redux-thunk、redux-saga）。
日志记录、错误捕获、API 请求拦截。
RTK Query 相比传统 Redux 异步处理的优势？
自动缓存与失效机制。
减少样板代码（无需手动编写 action 和 reducer）。
内置轮询、分页、乐观更新等功能。
七、总结
在 React 项目中使用 Redux 时，我采用以下策略：
使用 Redux Toolkit：简化状态管理，减少样板代码。
按功能模块组织代码：提高可维护性，避免文件过度集中。
优先使用 RTK Query：处理 API 请求，自动管理缓存和状态。
规范化状态结构：使用createEntityAdapter优化数据组织。
严格测试：确保状态逻辑正确性。
这种方法使代码结构清晰、可维护性高，并充分发挥 Redux 的优势。`
              }
            ]
          },
          {
            question: `29.说说对Redux中间件的理解？常用的中间件有哪些？实现原理？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Redux 中间件是用于扩展 Redux 功能的插件系统，主要用于处理异步操作、日志记录和副作用管理。以下是详细解析：
一、核心概念
1. 中间件的作用
拦截 action：在 action 到达 reducer 之前进行拦截和处理。
处理异步操作：如 API 请求、定时器等。
增强 Redux 功能：添加日志、错误处理、路由集成等。
2. 中间件的执行流程
plaintext
dispatch(action) → Middleware 1 → Middleware 2 → ... → Reducer
二、常用中间件
1. redux-thunk
作用：支持 action 返回函数（而非对象），处理异步逻辑。
示例：
jsx
const fetchUser = (userId) => {
  return async (dispatch) => {
    dispatch({ type: 'FETCH_USER_START' });
    try {
      const response = await fetch(‘/ api / users / ￥{ userId }’);
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: response.data });
    } catch (error) {
      dispatch({ type: 'FETCH_USER_FAILURE', payload: error.message });
    }
  };
};

2. redux-saga
作用：使用 Generator 函数管理异步操作，集中处理副作用。
示例：
jsx
import { call, put, takeEvery } from 'redux-saga/effects';

function* fetchUser(action) {
  try {
    const user = yield call(api.fetchUser, action.payload.userId);
    yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (e) {
    yield put({ type: 'FETCH_USER_FAILURE', payload: e.message });
  }
}

export function* rootSaga() {
  yield takeEvery('FETCH_USER_REQUEST', fetchUser);
}

3. redux-promise
作用：支持 action 返回 Promise，自动处理 Promise 的 resolve 和 reject。
示例：
jsx
const fetchUser = (userId) => {
  return {
    type: 'FETCH_USER',
    payload: fetch(‘/ api / users / ￥{ userId }’).then((res) => res.json()),
  };
};

4. redux-persist
作用：将 store 持久化到本地存储（如 localStorage）。
配置示例：
jsx
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
  key: 'root',
  storage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

5. redux-logger
作用：记录 action 和 state 的变化，用于调试。
配置示例：
jsx
import { applyMiddleware } from 'redux';
import logger from 'redux-logger';

const store = createStore(
  rootReducer,
  applyMiddleware(logger)
);

三、中间件实现原理
1. 中间件的基本结构
中间件是一个三层嵌套函数，接收store、next（指向下一个中间件的 dispatch）和action：
javascript
const middleware = (store) => (next) => (action) => {
  // 在action到达reducer前执行
  console.log('Dispatching action:', action);
  
  // 调用下一个中间件或reducer
  const result = next(action);
  
  // 在reducer处理后执行
  console.log('New state:', store.getState());
  
  return result;
};
2. 异步中间件（如 redux-thunk）的原理
判断 action 类型：如果 action 是函数，则执行该函数并传入 dispatch 和 getState。
示例实现：
javascript
const thunkMiddleware = (store) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

3. 中间件的注册流程
使用applyMiddleware将中间件应用到 store：
javascript
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk, logger)
);

四、中间件的执行顺序
中间件的注册顺序决定了它们的执行顺序：
javascript
applyMiddleware(middleware1, middleware2, middleware3);

// 执行顺序：
// middleware1 → middleware2 → middleware3 → reducer
五、常见面试题扩展
为什么需要中间件？直接在组件中处理异步逻辑不行吗？
集中管理：将异步逻辑放在中间件中，避免组件臃肿。
可测试性：中间件可独立测试，与组件解耦。
复用性：中间件逻辑可在多个组件间复用。
redux-thunk 和 redux-saga 的对比？
特性	redux-thunk	redux-saga
实现方式	简单函数	Generator 函数
学习曲线	低	高
异步控制	适合简单场景	适合复杂流程控制（如流程编排）
调试难度	较难追踪异步流程	易于调试和测试
代码量	少	多（需额外文件和模板代码）
如何自定义中间件？
实现三层嵌套函数结构：
javascript
const myMiddleware = (store) => (next) => (action) => {
  // 前置处理
  const result = next(action);
  // 后置处理
  return result;
};

中间件中的 next (action) 和 store.dispatch (action) 的区别？
next(action)：将 action 传递给下一个中间件或 reducer。
store.dispatch(action)：重新触发整个中间件链，可能导致无限循环。
六、总结
Redux 中间件是 Redux 生态的核心扩展机制，其主要作用包括：
处理异步操作：如 redux-thunk、redux-saga。
增强调试能力：如 redux-logger。
状态持久化：如 redux-persist。
统一副作用管理：集中处理 API 请求、路由等。
理解中间件的设计模式和执行流程，有助于构建可扩展、易维护的 React/Redux 应用。`
              }
            ]
          },
          {
            question: `30.说说你对immutable的理解？如何应用在react项目中？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `对 Immutable 的理解
Immutable（不可变数据）是指一旦创建就不能被修改的数据结构。任何对其的修改（如添加、删除、更新）都会返回一个全新的数据副本，而原数据保持不变。
核心特点：
不可变性：原数据不可修改，避免意外的副作用（如引用类型的 “共享修改” 问题）。
结构共享：新数据与原数据共享未被修改的部分，减少内存占用（类似 “Copy-on-Write” 机制）。
可追踪性：由于数据不可变，可通过对比引用快速判断数据是否变化（无需深拷贝对比）。
Immutable 在 React 项目中的应用场景与价值
React 的核心思想是 “数据驱动视图”，而 Immutable 能解决 React 中因引用类型数据修改导致的问题，具体应用如下：
1. 解决 React 状态更新的 “陷阱”
React 中，setState 或 useState 依赖引用变化来触发重新渲染。如果直接修改原状态（如数组 push、对象属性赋值），由于引用未变，React 会认为状态未更新，导致视图不刷新。
示例：错误的修改方式（引用未变）
javascript
const [list, setList] = useState([1, 2, 3]);
// 直接修改原数组，引用不变，视图不更新
const handleAdd = () => {
  list.push(4); 
  setList(list); // 无效更新
};
使用 Immutable 解决：
通过 Immutable 数据结构（如 Immutable.List），修改时返回新引用，确保 React 能感知变化：
javascript
import { List } from 'immutable';

const [list, setList] = useState(List([1, 2, 3]));
// 修改时返回新列表，引用变化，触发重渲染
const handleAdd = () => {
  const newList = list.push(4); 
  setList(newList); // 有效更新
};
2. 优化 React 组件性能
React 的 shouldComponentUpdate 或 React.memo 默认通过浅比较判断是否重渲染。对于复杂对象 / 数组，浅比较可能误判（引用不变但内容变），或因深比较耗时影响性能。
Immutable 数据可通过引用对比快速判断是否变化，避免不必要的重渲染：
示例：配合 React.memo 优化
javascript
import { is } from 'immutable';

// 子组件：使用 is 对比 Immutable 数据
const Child = React.memo(({ data }) => {
  return <div>{data.get('name')}</div>;
}, (prevProps, nextProps) => {
  // 引用对比，效率远高于深比较
  return is(prevProps.data, nextProps.data); 
});

// 父组件：传递 Immutable 对象
const Parent = () => {
  const [user, setUser] = useState(Map({ name: 'Alice' }));
  const handleUpdate = () => {
    // 修改后返回新对象，引用变化
    setUser(user.set('name', 'Bob')); 
  };
  return <Child data={user} />;
};
3. 简化 Redux 状态管理
Redux 要求状态（state）是只读的，修改必须通过 action 和 reducer 返回新状态。Immutable 与 Redux 的 “纯函数” 理念高度契合：
避免直接修改 state（符合 Redux 规范）；
通过 reducer 返回新状态时，利用结构共享减少内存开销；
结合 reselect 等库，通过引用对比快速计算派生数据。
示例：Redux Reducer 中使用 Immutable
javascript
import { fromJS } from 'immutable';

// 初始状态（转为 Immutable.Map）
const initialState = fromJS({
  todos: [],
  loading: false
});

// Reducer：返回新状态，原状态不变
const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      // 结构共享，仅新增项变化
      return state.update('todos', todos => todos.push(action.payload));
    default:
      return state;
  }
};
4. 常用 Immutable 库
React 项目中常用的 Immutable 库：
Immutable.js：提供完整的不可变数据结构（Map、List、Set 等），API 丰富，但需学习新语法。
Immer：通过 “draft 草稿” 模式，允许 “直接修改” 的写法，内部自动转换为不可变数据，学习成本低（推荐新手）。
Immer 示例（更符合直觉）：
javascript
import { produce } from 'immer';

const [user, setUser] = useState({ name: 'Alice', age: 20 });

const handleUpdate = () => {
  // 看似“修改”，实际返回新对象
  const newUser = produce(user, draft => {
    draft.age = 21; // 直接修改草稿
  });
  setUser(newUser);
};



总结
Immutable 核心：数据不可变，修改返回新引用，解决引用类型共享修改的问题。
React 中价值：确保状态更新被感知、优化组件重渲染性能、契合 Redux 纯函数理念。
实践建议：简单场景用 Immer（低学习成本），复杂场景用 Immutable.js（功能更全）。`
              }
            ]
          },
          {
            question: `31.说说React服务端渲染怎么做？原理是什么?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `React 服务端渲染（SSR）介绍
服务端渲染（Server-Side Rendering）是一种将 React 组件在服务器端预渲染成 HTML，再发送到客户端的技术。与传统的客户端渲染（CSR）相比，SSR 能提供更好的 SEO、更快的首屏加载速度和更好的用户体验。
核心原理
React 服务端渲染的核心原理可以概括为以下步骤：
服务器接收请求：当用户访问页面时，请求首先到达服务器。
渲染 React 组件：服务器使用 Node.js 环境，将 React 组件渲染为 HTML 字符串。
注入初始数据：如果组件需要数据，可以在服务器端获取数据，并将数据嵌入到 HTML 中。
发送 HTML 到客户端：服务器将预渲染的 HTML 和初始数据发送到客户端。
客户端激活：客户端接收到 HTML 后，React 会使用相同的组件结构和数据，在浏览器中 "激活" 静态 HTML，使其成为可交互的应用。
实现方式
React 服务端渲染有多种实现方式，以下是常见的几种：
1. 手动实现 SSR
可以使用 React 的renderToString或renderToStaticMarkup方法手动实现 SSR。这种方式需要自己搭建服务器（如 Express），处理路由和状态管理。
javascript
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('*', (req, res) => {
  // 渲染React组件为HTML
  const html = renderToString(<App />);
  
  // 返回完整的HTML页面
  res.send(‘
                  < !DOCTYPE html>
              <html>
                <head>
                  <title>React SSR</title>
                </head>
                <body>
                  <div id="root">￥{html}</div>
                  《script>script src="client.js">《/script>
        </body>
        
        </html>
        ’);
        });
        
        app.listen(3000, () => {
        console.log('Server running on port 3000');
        });
        2. 使用 Next.js
        Next.js 是一个流行的 React SSR 框架，它简化了 SSR 的实现，提供了自动代码分割、路由系统和预渲染等功能。
        javascript
        // pages/index.js
        import React from 'react';
        
        const HomePage = ({ data }) => {
        return (
        <div>
          <h1>Welcome to Next.js!</h1>
          <p>Data from server: {data}</p>
        </div>
        );
        };
        
        // 服务器端获取数据
        export async function getServerSideProps() {
        const res = await fetch('https://api.example.com/data');
        const data = await res.json();
        
        return {
        props: {
        data
        }
        };
        }
        
        export default HomePage;
        3. 使用 Gatsby
        Gatsby 是一个基于 React 的静态网站生成器，也支持 SSR。它主要用于构建静态网站，但也可以处理动态内容。
        关键技术点
        数据流管理：在 SSR 中，需要确保服务器和客户端使用相同的数据。可以使用 Redux、Context API 或 SWR 等库来管理状态。
        生命周期差异：某些 React 生命周期方法（如 componentDidMount）只在客户端执行，需要注意在 SSR 环境中的使用。
        样式处理：确保 CSS 在服务器端正确渲染。可以使用 styled-components、emotion 等支持 SSR 的 CSS-in-JS 解决方案。
        客户端激活：客户端接收到预渲染的 HTML 后，React 需要 "hydrate" 这些 HTML，使其变为交互式应用。
        优缺点
        优点：
        更好的 SEO，搜索引擎可以直接抓取预渲染的 HTML 内容
        更快的首屏加载速度，用户可以更快看到内容
        对不支持 JavaScript 的浏览器友好
        缺点：
        服务器负载增加，需要更多服务器资源
        开发复杂度提高，需要处理服务器和客户端环境差异
        构建和部署流程更复杂
        适用场景
        需要良好 SEO 的网站（如博客、电商网站）
        内容型网站，首屏加载速度至关重要
        对搜索引擎可见性要求高的应用
        通过理解 React 服务端渲染的原理和实现方式，可以在面试中清晰地阐述这一概念，并结合具体项目经验进行回答。`
              }
            ]
          }
        ]
      },
      {
        tag: `Webpack面试题`,
        data: [
          {
            question: `1.说说你对webpack的理解？解决了什么问题?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `对 Webpack 的理解
Webpack 是一个模块打包工具，核心功能是将多个模块（JS、CSS、图片等）打包成一个或多个文件（bundle），解决了现代前端开发中的模块化、资源管理、性能优化等问题。
Webpack 解决的核心问题
1. 模块化打包
问题：前端项目中模块数量激增（如 ES6 modules、CommonJS），浏览器无法直接处理复杂的模块依赖关系。
解决方案：Webpack 分析模块间的依赖关系，将所有模块打包成一个或多个 bundle，减少浏览器的 HTTP 请求次数。
2. 处理非 JS 资源
问题：CSS、图片、字体等资源无法直接作为模块被 JS 引用。
解决方案：通过 loader（如 css-loader、file-loader）将非 JS 资源转换为 JS 模块，统一管理。
3. 代码分割与懒加载
问题：单页应用（SPA）的 bundle 文件过大，导致首屏加载缓慢。
解决方案：通过 splitChunks 和动态导入（import()）实现代码分割，按需加载模块（如路由懒加载）。
4. 开发体验优化
问题：开发过程中频繁刷新页面、手动处理文件变化。
解决方案：
DevServer：提供热更新（HMR），修改代码后无需刷新页面。
Source Map：定位打包后代码的原始位置，方便调试。
5. 生产环境优化
问题：代码未压缩、资源未优化，导致线上性能差。
解决方案：
压缩代码：通过 terser-webpack-plugin 压缩 JS/CSS。
资源优化：图片压缩、CSS 提取（mini-css-extract-plugin）、Tree Shaking。
6. 统一资源处理流程
问题：不同类型的资源需要不同的处理方式（如 CSS 预处理器、JS 转译）。
解决方案：通过 loader 和 plugin 组合，自定义资源处理流程（如 Babel 转译、SCSS 编译）。
核心概念与工作流程
1. Entry & Output
Entry：打包的入口文件，Webpack 从这里开始分析依赖。
Output：打包后的文件输出路径和命名规则。
2. Loader
处理非 JS 模块，如：
babel-loader：转译 ES6+ 语法。
css-loader/style-loader：处理 CSS 并注入到 DOM。
url-loader：处理图片，小文件转为 base64 减少请求。
3. Plugin
扩展 Webpack 功能，如：
HtmlWebpackPlugin：自动生成 HTML 文件并注入 bundle。
CleanWebpackPlugin：打包前清理输出目录。
DefinePlugin：定义环境变量。
4. Mode
预设配置：development（开发模式）和 production（生产模式，默认开启压缩）。
5. 工作流程
plaintext
Entry → 模块解析（Loader 处理） → 依赖图构建 → 资源处理（Plugin） → Output
实际应用场景
1. 多页面应用（MPA）打包
通过配置多个 entry 点，为每个页面生成独立的 bundle：
javascript
// webpack.config.js
module.exports = {
  entry: {
    index: './src/index.js',
    about: './src/about.js'
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist')
  }
};
2. CSS 处理与提取
使用 mini-css-extract-plugin 将 CSS 提取为独立文件：
javascript
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin()]
};
3. 图片优化
使用 image-webpack-loader 压缩图片：
javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: { name: '[name].[ext]' }
          },
          {
            loader: 'image-webpack-loader',
            options: { mozjpeg: { quality: 80 } }
          }
        ]
      }
    ]
  }
};

总结
Webpack 是现代前端工程化的基石，通过模块化打包、资源处理、性能优化三大核心能力，解决了前端开发中的复杂度和性能问题。它的灵活性（loader/plugin 机制）使其能适应各种项目需求，成为前端构建的事实标准。`
              }
            ]
          },
          {
            question: `2.说说webpack的热更新是如何做到的？原理是什么?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack 热更新（HMR）的原理
Webpack 热更新（Hot Module Replacement，简称 HMR）是指在应用运行时无需刷新整个页面，就能替换、添加或删除模块，极大提升了开发体验。其核心原理基于客户端与服务端的实时通信和模块热替换逻辑。
核心工作流程
1. 初始化阶段
Webpack 编译：
Webpack 构建时会生成两份资源：
JS bundle：包含应用代码和 HMR 客户端（通过 webpack-dev-server 注入）。
更新信息文件：记录模块的哈希值和依赖关系（存于内存或磁盘）。
DevServer 启动：
启动 HTTP 服务器，同时创建 WebSocket 服务（用于与浏览器通信）。
2. 运行时通信阶段
客户端连接：
浏览器加载页面时，HMR 客户端通过 WebSocket 连接到 DevServer。
文件变更监听：
DevServer 监听文件系统变化，当文件修改时：
Webpack 重新编译受影响的模块。
生成 更新包（包含新模块代码和哈希值）。
推送更新通知：
DevServer 通过 WebSocket 发送 更新通知（Update Notification）到客户端，携带更新包的哈希值。
3. 模块替换阶段
客户端请求更新：
HMR 客户端收到通知后，向 DevServer 请求 更新包（通过 JSONP 或直接下载）。
应用更新：
HMR 运行时卸载旧模块，加载新模块，并执行 自定义更新逻辑（如 React 的状态保持）。
回调触发：
模块可通过 module.hot.accept 注册回调，处理自身更新逻辑（如重新渲染组件）。
关键技术组件
1. WebSocket 通信
用于实时推送文件变更和更新通知，避免轮询带来的性能损耗。
2. 内存文件系统
DevServer 将编译后的文件存储在内存中（而非磁盘），提升响应速度。
3. HMR 运行时代码
Webpack 在打包时注入的特殊代码，负责管理模块的加载、卸载和更新逻辑。
4. 模块标识符
每个模块被赋予唯一 ID，确保更新时能准确定位和替换。
示例：React 组件的 HMR 实现
React 组件的 HMR 需要借助 react-hot-loader 或 @pmmmwh/react-refresh-webpack-plugin，原理是：
保留组件状态：
通过 module.hot.accept 捕获组件更新，重新渲染但保留 state。
注入刷新逻辑：
在开发环境中，为组件包裹 HMR 代理，实现无刷新更新。
javascript
// 组件代码
import React from 'react';

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
};

// HMR 支持（使用 React Fast Refresh）
if (module.hot) {
  module.hot.accept();
}
优势与限制
优势
保留应用状态：如表单输入、滚动位置等。
提升开发效率：无需刷新页面，减少等待时间。
精准定位问题：错误信息更直观，避免全局刷新导致的上下文丢失。
限制
需要特定实现：部分模块（如 CSS、复杂状态管理）需额外配置。
不完全可靠：极端情况下可能需要手动刷新（如修改非 HMR 模块）。
总结
Webpack HMR 的核心是通过 WebSocket 实时通信 和 模块动态替换，实现开发阶段的高效更新。其原理可概括为：
监听文件变更 → 2. 编译生成更新包 → 3. WebSocket 推送通知 → 4. 客户端请求并应用更新。
理解 HMR 原理有助于排查开发中的问题，也能更好地配置和优化构建流程。`
              }
            ]
          },
          {
            question: `3. 说说webpack的构建流程?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack 的构建流程是一个按顺序执行的流水线式过程，核心是从入口文件出发，递归解析所有模块的依赖关系，经过一系列处理后，最终输出打包后的文件。整个流程可分为 初始化、编译、输出 三大阶段，每个阶段包含多个关键步骤。
详细构建流程（分阶段解析）
1. 初始化阶段：准备工作
目标：读取配置参数，初始化 Compiler 实例，确定构建入口。
关键步骤：
参数合并：
Webpack 会合并默认配置、用户配置（webpack.config.js）、CLI 命令行参数（如 --mode production），生成最终的构建配置。
创建 Compiler 实例：
Compiler 是 Webpack 的核心对象，负责统筹整个构建流程，包含配置信息、插件集合、生命周期钩子等。
加载插件：
执行所有插件的 apply 方法，让插件注册到 Compiler 的生命周期钩子中（插件可通过钩子干预构建过程）。
确定入口：
根据配置的 entry 找到所有入口文件（如 ./src/index.js），作为构建的起点。
2. 编译阶段：解析与转换模块
目标：递归解析所有模块的依赖关系，生成依赖图谱，并对模块进行转换处理。
关键步骤：
创建 Compilation 实例：
Compilation 是单次构建的上下文对象，包含当前构建的模块、依赖、输出资源等信息（每次文件变化触发重新构建时，会创建新的 Compilation）。
解析入口模块：
从入口文件开始，调用 loader 对模块进行转换（如用 babel-loader 转译 ES6+ 语法，css-loader 处理 CSS）。
构建依赖图谱：
解析转换后的模块代码，提取依赖声明（如 import、require），递归解析依赖的模块，形成完整的模块依赖树。
模块优化：
执行插件的优化逻辑（如 Tree Shaking 剔除未使用的代码）。
对模块进行合并、拆分（如 splitChunks 拆分公共模块）。
3. 输出阶段：生成最终资源
目标：将处理后的模块打包成最终的输出文件（bundle），写入磁盘或内存。
关键步骤：
生成 Chunk：
根据依赖图谱和配置（如 optimization.splitChunks），将模块组合成 Chunk（代码块），每个 Chunk 对应一个或多个输出文件。
渲染 Chunk：
将 Chunk 中的模块代码拼接成可执行的 JS 代码（处理模块封装、依赖注入等逻辑）。
生成输出资源：
根据 output 配置（路径、文件名规则），将 Chunk 写入文件系统（开发环境下通常存于内存，通过 webpack-dev-server 访问）。
触发完成钩子：
执行 done 等钩子，通知插件构建完成（如 HtmlWebpackPlugin 生成 HTML 并注入 bundle）。
核心对象与钩子机制
Compiler：全局唯一，贯穿整个构建生命周期，暴露 run、watch、done 等钩子。
Compilation：对应单次构建过程，暴露 compile、optimize、emit 等钩子，插件可通过这些钩子修改模块或资源。
钩子（Hooks）：Webpack 基于 Tapable 库实现钩子机制，插件通过钩子介入构建流程（如 emit 钩子可在输出前修改资源）。
简化流程图
plaintext
初始化阶段
  ├─ 合并配置 → 创建 Compiler → 加载插件 → 确定入口
  ↓
编译阶段
  ├─ 创建 Compilation → 解析入口模块 → 调用 loader 转换
  ├─ 递归解析依赖 → 构建依赖图谱
  └─ 优化模块（Tree Shaking、拆分 Chunk）
  ↓
输出阶段
  ├─ 生成 Chunk → 渲染 Chunk 为可执行代码
  ├─ 写入输出文件（内存/磁盘）
  └─ 触发 done 钩子，构建完成

总结
Webpack 的构建流程是从配置解析到模块处理，再到资源输出的流水线过程，核心依赖 Compiler 和 Compilation 两大对象，通过插件钩子机制实现灵活扩展。理解这一流程有助于排查构建问题（如 loader 失效、打包体积过大），并能针对性地优化构建性能（如减少不必要的模块解析、合理配置缓存）。`
              }
            ]
          },
          {
            question: `4.说说webpackproxy工作原理？为什么能解决跨域?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack Proxy 的工作原理与跨域解决方案
1. 跨域问题的本质
跨域（CORS）是浏览器的安全机制，仅限制浏览器端发起的请求。当浏览器向不同域名的服务器发送请求时，若服务器未明确允许（即未设置 Access-Control-Allow-Origin 等响应头），浏览器会拦截响应。
关键点：
服务器之间的请求不存在跨域限制（如 Node.js 后端调用其他 API）。
跨域问题仅在开发环境中影响前端调试，生产环境通常通过后端代理或 CORS 配置解决。
2. Webpack Proxy 的核心原理
Webpack DevServer 提供的 proxy 功能，本质是一个中间层代理服务器，工作流程如下：
浏览器请求 → DevServer
前端代码（如 fetch('/api/data')）发送请求到 DevServer（通常是 http://localhost:3000）。
DevServer → 目标 API 服务器
DevServer 识别到请求路径匹配 proxy 规则时，以服务器身份转发请求到真正的 API 服务器（如 http://api.example.com）。
API 响应 → DevServer → 浏览器
API 服务器返回响应给 DevServer，DevServer 再将响应返回给浏览器，此时请求的源是 DevServer（同源），浏览器不再拦截。
3. 示例配置与流程
Webpack 配置
javascript
// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com',  // 目标 API 服务器
        changeOrigin: true,                // 修改请求头中的 Origin
        pathRewrite: { '^/api': '' }       // 重写路径，如 /api/users → /users
      }
    }
  }
};
请求流程
前端代码：
javascript
fetch('/api/users')  // 浏览器请求 http://localhost:3000/api/users
  .then(res => res.json());

DevServer 转发：
DevServer 收到请求后，将其转发至 http://api.example.com/users。
添加 Origin: http://localhost:3000 请求头（若 changeOrigin: true）。
API 响应：
API 服务器返回数据给 DevServer。
DevServer 将响应返回给浏览器，浏览器认为请求来自同源（localhost:3000），不再拦截。
4. 关键配置参数解析
target：目标服务器地址，请求将被转发到此地址。
changeOrigin：
默认为 false，转发时保留原请求的 Origin（如 localhost:3000）。
设为 true 时，将 Origin 修改为目标服务器地址（如 api.example.com），避免某些 API 服务器拒绝跨域请求。
pathRewrite：重写请求路径，常用于移除前缀（如 /api）。
secure：是否验证 SSL 证书（默认 true）。
5. 为什么能解决跨域？
绕过浏览器限制：请求实际上由 DevServer（Node.js 服务器）发起，而非浏览器，因此不受同源策略限制。
同源响应：浏览器接收的响应来自 DevServer（同源），而非实际的 API 服务器，因此不会触发跨域拦截。
6. 生产环境如何处理？
开发环境使用 Webpack Proxy 解决跨域，生产环境通常有两种方案：
后端代理：
前端请求发送到同域名的后端服务，由后端转发至实际 API（如 Nginx 反向代理）。
CORS 配置：
API 服务器添加响应头（如 Access-Control-Allow-Origin: *），直接允许跨域请求。
总结
Webpack Proxy 通过中间层服务器转发请求，将浏览器的跨域请求转化为服务器间的同源请求，从而避免浏览器的安全拦截。这是开发环境中最常用的跨域解决方案，优点是无需修改后端代码，缺点是仅适用于开发阶段。`
              }
            ]
          },
          {
            question: `5.说说webpack中常见的Loader?解决了什么问题?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack 中常见的 Loader 及其作用
Webpack 的核心是将所有资源（JS、CSS、图片等）视为模块，但原生 Webpack 仅支持处理 JS 文件。Loader 的作用是将不同类型的文件转换为 Webpack 可处理的模块，解决了资源模块化的问题。以下是常见 Loader 及其解决的问题：
1. JS 处理类 Loader
babel-loader
问题：现代 JS 语法（如 ES6+、JSX）在旧浏览器中不兼容。
解决方案：使用 Babel 将代码转译为向后兼容的版本。
配置示例：
javascript
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: { presets: ['@babel/preset-env', '@babel/preset-react'] }
  }
}

ts-loader 或 awesome-typescript-loader
问题：TypeScript 代码需要编译为 JS。
解决方案：将 TS 文件转换为 JS。
依赖：需配合 tsconfig.json 使用。
2. CSS 处理类 Loader
style-loader + css-loader
问题：CSS 无法直接被 JS 引用，且缺少模块化能力。
解决方案：
css-loader：解析 CSS 文件中的 @import 和 url()，处理依赖关系。
style-loader：将 CSS 注入到 DOM（通过 <style> 标签）。
配置示例：
javascript
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader'] // 注意顺序：从右到左执行
}

sass-loader / less-loader
问题：Sass/Less 预处理器语法需编译为 CSS。
解决方案：
javascript
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

postcss-loader
问题：CSS 兼容性（如添加浏览器前缀）、CSS 优化。
解决方案：配合 autoprefixer 等插件处理 CSS。
javascript
{
  test: /\.css$/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: { plugins: [require('autoprefixer')] }
    }
  ]
}

mini-css-extract-plugin
问题：开发环境中 CSS 通过 JS 注入，导致运行时开销；生产环境需要单独的 CSS 文件。
解决方案：将 CSS 提取为独立文件（替代 style-loader）。
javascript
{
  test: /\.css$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader']
}

3. 图片与文件处理类 Loader
file-loader
问题：图片、字体等资源无法直接被 JS 引用。
解决方案：将文件复制到输出目录，并返回引用路径。
javascript
{
  test: /\.(png|jpg|gif)$/,
  use: {
    loader: 'file-loader',
    options: { name: '[name].[ext]' }
  }
}

url-loader
问题：小文件（如图标）会增加 HTTP 请求数。
解决方案：将小文件转为 base64 内联到代码中，减少请求。
javascript
{
  test: /\.(png|jpg|gif)$/,
  use: {
    loader: 'url-loader',
    options: { limit: 8192 } // 小于 8KB 的文件转为 base64
  }
}

image-webpack-loader
问题：图片体积过大，影响加载速度。
解决方案：压缩图片（如 JPEG、PNG）。
javascript
{
  test: /\.(png|jpg|gif)$/,
  use: [
    'file-loader',
    {
      loader: 'image-webpack-loader',
      options: { mozjpeg: { quality: 80 } }
    }
  ]
}

4. 其他类型 Loader
json-loader
问题：Webpack 4+ 已内置，无需配置，用于处理 JSON 文件。
html-loader
问题：HTML 中的资源路径（如 <img src>）无法被 Webpack 解析。
解决方案：处理 HTML 中的资源引用。
javascript
{
  test: /\.html$/,
  use: {
    loader: 'html-loader',
    options: { attributes: { list: [{ tag: 'img', attribute: 'src', type: 'src' }] } }
  }
}

raw-loader
问题：需要将文件内容作为字符串导入（如文本、SVG）。
解决方案：返回文件的原始内容。
javascript
{
  test: /\.txt$/,
  use: 'raw-loader'
}


5. 总结
Loader 的核心作用是将不同类型的资源转换为 Webpack 可处理的模块，实现资源的统一管理和打包。常见场景包括：
代码转换（如 Babel、TypeScript）。
样式处理（如 CSS、Sass 编译与注入）。
资源优化（如图片压缩、文件内联）。
理解 Loader 的分工和执行顺序（从右到左 / 从下到上）是配置 Webpack 的关键。`
              }
            ]
          },
          {
            question: `6.说说webpack中常见的Plugin?解决了什么问题?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack 中常见的 Plugin 及其作用
Webpack 的 Plugin 是用于扩展其功能的工具，通过监听生命周期钩子（如编译完成、资源输出前）来干预构建过程。与 Loader 不同，Plugin 不直接处理文件转换，而是专注于优化打包结果、注入环境变量、管理输出文件等全局性任务。以下是常见的 Plugin 及其解决的问题：
1. 资源管理类 Plugin
HtmlWebpackPlugin
问题：手动在 HTML 中引入打包后的 JS/CSS 文件繁琐，且文件名可能包含哈希值（如 main.[hash].js）。
解决方案：自动生成 HTML 文件并注入打包后的资源路径。
配置示例：
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins = [
  new HtmlWebpackPlugin({
    template: './src/index.html', // 模板文件
    minify: true // 压缩 HTML
  })
];

CleanWebpackPlugin
问题：每次构建时，旧的打包文件（如 dist 目录）可能残留，导致混淆。
解决方案：在打包前清理输出目录。
javascript
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

plugins = [new CleanWebpackPlugin()];

MiniCssExtractPlugin
问题：开发环境中 CSS 通过 JS 注入（style-loader），生产环境需要单独的 CSS 文件以提高加载性能。
解决方案：将 CSS 提取为独立文件（替代 style-loader）。
javascript
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

plugins = [
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash].css'
  })
];

2. 优化与压缩类 Plugin
TerserPlugin
问题：打包后的 JS 文件体积过大，影响加载速度。
解决方案：压缩和混淆 JS 代码（Webpack 5+ 默认内置）。
javascript
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin()]
  }
};

OptimizeCSSAssetsPlugin
问题：CSS 文件未压缩。
解决方案：压缩提取后的 CSS 文件。
javascript
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin()]
  }
};

BundleAnalyzerPlugin
问题：打包后的文件体积过大，但不清楚哪些模块占比高。
解决方案：生成可视化报告，分析 bundle 组成。
javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

plugins = [new BundleAnalyzerPlugin()];

3. 环境与变量处理类 Plugin
DefinePlugin
问题：需要在代码中注入环境变量（如区分开发 / 生产环境）。
解决方案：定义全局常量，在编译时替换。
javascript
const webpack = require('webpack');

plugins = [
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
  })
];

EnvironmentPlugin
问题：从 process.env 读取环境变量并注入，简化 DefinePlugin 的配置。
javascript
plugins = [
  new webpack.EnvironmentPlugin(['API_KEY', 'BASE_URL'])
];

4. 代码分割与懒加载类 Plugin
CommonsChunkPlugin（Webpack 4-）/ splitChunks
问题：多个 bundle 中重复引入相同模块，导致代码冗余。
解决方案：提取公共模块为单独的 chunk（Webpack 4+ 使用 optimization.splitChunks 替代）。
javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all' // 提取所有类型的 chunk（async、initial、all）
    }
  }
};

DynamicImportsPlugin
问题：动态导入（import()）需要额外支持。
解决方案：支持 ES6 动态导入语法（Webpack 4+ 默认支持，无需配置）。
5. 其他常用 Plugin
HotModuleReplacementPlugin（HMR）
问题：开发过程中修改代码需要刷新整个页面，丢失应用状态。
解决方案：实现热更新，仅替换变更的模块。
javascript
plugins = [new webpack.HotModuleReplacementPlugin()];

CopyWebpackPlugin
问题：需要将未经过处理的文件（如静态资源）复制到输出目录。
解决方案：复制文件 / 目录到打包结果中。
javascript
const CopyWebpackPlugin = require('copy-webpack-plugin');

plugins = [
  new CopyWebpackPlugin({
    patterns: [{ from: 'public', to: 'public' }]
  })
];

WorkboxWebpackPlugin
问题：需要为应用添加 Service Worker 以支持离线缓存。
解决方案：自动生成 Service Worker 配置（基于 Google Workbox）。
javascript
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');

plugins = [new WorkboxWebpackPlugin.GenerateSW()];



总结
Plugin 的核心作用是在 Webpack 构建流程的特定阶段执行自定义逻辑，解决诸如资源管理、代码优化、环境配置等全局性问题。常见场景包括：
自动化：自动生成 HTML、清理旧文件。
性能优化：压缩代码、分割 Chunk、分析 bundle。
功能扩展：热更新、环境变量注入、离线支持。
理解 Plugin 的工作机制（监听生命周期钩子）是配置复杂 Webpack 项目的关键。`
              }
            ]
          },
          {
            question: `7.说说Loader和Plugin的区别?编写Loader,Plugin的思路？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Loader 与 Plugin 的核心区别
                1. 职责不同
Loader：
单一文件转换：将特定类型的文件（如 CSS、TS）转换为 Webpack 可处理的模块。
链式调用：多个 Loader 按顺序处理同一个文件（如 sass-loader → css-loader → style-loader）。
Plugin：
扩展构建流程：监听 Webpack 生命周期钩子，在特定阶段执行自定义逻辑（如打包优化、资源管理）。
全局作用：影响整个构建过程，而非单个文件。
2. 工作时机不同
Loader：在模块解析阶段处理单个文件。
Plugin：在构建流程的特定钩子（如编译完成、资源输出前）执行。
3. 配置方式不同
Loader：在 module.rules 中配置，针对特定文件类型。
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader'] // 从右到左执行
}
Plugin：在 plugins 数组中实例化并配置。
javascript
plugins = [
  new HtmlWebpackPlugin({ template: './src/index.html' }),
  new CleanWebpackPlugin()
];


编写 Loader 的思路
核心原则
单一职责：每个 Loader 只做一件事（如转译、压缩）。
无状态：相同输入应返回相同输出，不依赖外部状态。
链式调用：输出应为 JS 代码或可被下一个 Loader 处理的格式。
基本结构
javascript
// my-loader.js
module.exports = function(source) {
  // source：文件内容（字符串或 Buffer）
  // 处理逻辑（如替换文本、转译代码）
  const result = source.replace(/Hello/g, 'Hi');

  // 返回处理后的内容
  return result;
};
异步 Loader
javascript
module.exports = function(source) {
  const callback = this.async(); // 获取异步回调

  // 异步处理（如调用 API、读取文件）
  setTimeout(() => {
    const result = doSomething(source);
    callback(null, result); // 第一个参数为错误，第二个为结果
  }, 1000);
};

实战示例：Base64 转换 Loader
module.exports = function(source) {
  if (this.resourcePath.endsWith('.png')) {
    const base64 = Buffer.from(source).toString('base64');
    return 'export default "data:image/png;base64,￥{base64}";';
  }
  return source;
};
编写 Plugin 的思路
核心原则
监听钩子：通过 compiler 或 compilation 对象监听特定生命周期事件。
修改状态：在钩子中修改 Webpack 内部状态（如添加资源、修改输出）。
基本结构
javascript
// my-plugin.js
class MyPlugin {
  constructor(options) {
    this.options = options; // 接收配置参数
  }

  apply(compiler) {
    // 1. 监听 compiler 钩子（全局构建过程）
    compiler.hooks.compile.tap('MyPlugin', (params) => {
      console.log('开始编译');
    });

    // 2. 监听 compilation 钩子（单次编译过程）
    compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
      // 如：在生成资源前修改内容
      compilation.hooks.processAssets.tap('MyPlugin', (assets) => {
        // assets 是所有打包后的资源对象
        Object.keys(assets).forEach((filename) => {
          if (filename.endsWith('.js')) {
            const content = assets[filename].source();
            // 修改 JS 内容（如添加版本号）
            assets[filename] = new sources.RawSource(
              ’// Version: ￥{this.options.version}\n￥{content}‘
            );
          }
        });
      });
    });
  }
}

  module.exports = MyPlugin;


  常用钩子
  compiler 钩子：
compile：编译开始时触发。
emit：输出文件到目录前触发。
done：构建完成时触发。
compilation 钩子：
optimize：优化资源时触发。
processAssets：处理资源时触发。
afterEmit：输出资源后触发。
总结对比
特性	Loader	Plugin
核心功能	文件转换（如 CSS → JS 模块）	扩展构建流程（如生成 HTML、压缩）
作用范围	单个文件	整个构建过程
配置位置	module.rules	plugins 数组
执行顺序	从右到左 / 从下到上	按注册顺序执行
编写关键	处理文件内容，返回 JS 代码	监听钩子，修改 Webpack 内部状态
实战建议
Loader：
使用 loader-utils 处理参数（如 getOptions）。
使用 schema-utils 验证配置（如校验正则表达式）。
Plugin：
查阅 Webpack 官方文档了解完整钩子列表。
使用 tapAsync 或 tapPromise 处理异步操作。
通过合理组合 Loader 和 Plugin，可以实现高度定制化的构建流程，满足复杂项目的需求。
                `
              }
            ]
          },
          {
            question: `8.如何提高webpack的构建速度?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `提高 Webpack 构建速度的策略
Webpack 构建速度优化可从 缓存机制、并行处理、减少编译范围、配置调优 四个维度入手。以下是具体方法：
1. 利用缓存机制
1.1 babel-loader 缓存
原理：缓存 Babel 转译结果，避免重复编译未修改的文件。
配置：
javascript
{
  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true // 启用缓存
    }
  }
}

1.2 Webpack 持久化缓存
原理：将编译结果写入磁盘，下次构建时复用。
配置（Webpack 5+）：
javascript
module.exports = {
  cache: {
    type: 'filesystem', // 使用文件系统缓存
    buildDependencies: {
      config: [__filename] // 当配置文件变化时，重新缓存
    }
  }
};

2. 并行处理与多线程
2.1 thread-loader
原理：将耗时的 loader（如 Babel）放到 Worker 线程池执行。
配置：
javascript
{
  test: /\.js$/,
  use: [
    'thread-loader', // 放在耗时 loader 前
    'babel-loader'
  ]
}

注意：
每个 Worker 是独立进程，启动开销约 600ms，建议仅对大型项目使用。
限制 Worker 数量：
javascript
'thread-loader': { workers: 2 }

2.2 并行压缩
terser-webpack-plugin 开启多线程：
javascript
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true // 默认为 CPU 核心数 - 1
      })
    ]
  }
};

3. 减少编译范围
3.1 exclude/include
原理：明确指定 loader 处理的文件范围，避免冗余处理。
配置：
javascript
{
  test: /\.js$/,
  exclude: /node_modules/, // 排除 node_modules
  include: path.resolve(__dirname, 'src') // 仅处理 src 目录
}

3.2 noParse
原理：对无需解析依赖的模块（如 jQuery），跳过解析阶段。
配置：
javascript
module.exports = {
  module: {
    noParse: /jquery|lodash/, // 正则匹配
  }
};

3.3 resolve.modules
原理：减少模块搜索路径。
配置：
javascript
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')] // 避免向上搜索
  }
};

4. 配置调优
4.1 移除不必要的插件
开发环境禁用：
MiniCssExtractPlugin（用 style-loader 替代）。
TerserPlugin（无需压缩代码）。
4.2 动态导入与代码分割
原理：按需加载模块，减少初始构建体积。
示例：
javascript
// 动态导入组件
const loadComponent = async () => {
  const { MyComponent } = await import('./MyComponent');
  return <MyComponent />;
};

4.3 DLLPlugin
原理：将不常变化的库（如 React、Vue）预编译为单独的 DLL 文件。
配置：
创建 webpack.dll.js：
javascript
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.js',
    library: '[name]_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_[hash]',
      path: path.join(__dirname, '[name]-manifest.json')
    })
  ]
};

在主配置中引用：
javascript
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./vendor-manifest.json')
    })
  ]
};

5. 开发环境专属优化
5.1 热更新（HMR）
原理：仅更新修改的模块，无需全量构建。
配置：
javascript
module.exports = {
  devServer: {
    hot: true // 启用 HMR
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
};

5.2 开发环境配置简化
禁用 source-map 或使用 eval 模式：
javascript
module.exports = {
  devtool: 'eval' // 最快的 source-map 模式
};

6. 生产环境专属优化
6.1 并行构建
使用 webpack-merge 拆分配置，并行执行多个构建：
bash
webpack --config webpack.common.js --config webpack.prod.js

6.2 压缩工具选择
使用 esbuild-loader 替代 Babel/Terser（速度提升 10x+）：
javascript
{
  test: /\.js$/,
  loader: 'esbuild-loader',
  options: {
    target: 'es2015'
  }
}

7. 其他实用工具
7.1 speed-measure-webpack-plugin
作用：分析各 loader/plugin 的耗时，定位性能瓶颈。
配置：
javascript
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // 原始配置
});

7.2 webpack-bundle-analyzer
作用：可视化分析 bundle 体积，找出大模块。
总结优化清单
缓存：启用 babel-loader 和 Webpack 持久化缓存。
并行：使用 thread-loader 和并行压缩。
范围控制：合理配置 exclude/include 和 noParse。
开发环境：使用 HMR、简化 source-map。
生产环境：预编译 DLL、使用 esbuild 加速。
通过组合这些策略，大型项目的构建速度可提升 50% 以上。`
              }
            ]
          },
          {
            question: `9.说说如何借助webpack来优化前端性能?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `Webpack 优化前端性能的核心策略
Webpack 不仅是构建工具，更是前端性能优化的关键抓手。通过合理配置，可从 资源体积、加载速度、运行时性能 三个维度大幅提升应用体验。以下是具体优化方法：
1. 资源体积优化
1.1 代码压缩与混淆
插件：
TerserPlugin（JS 压缩）：Webpack 5+ 默认启用，支持多线程。
OptimizeCSSAssetsPlugin（CSS 压缩）。
配置示例：
javascript
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: { drop_console: true } // 移除 console
        }
      }),
      new OptimizeCSSAssetsPlugin()
    ]
  }
};

1.2 Tree Shaking
原理：移除未使用的代码（依赖 ES6 静态导入）。
配置：
确保 package.json 中设置 "sideEffects": false。
使用 mode: "production"（默认开启 Tree Shaking）。
示例：
javascript
// 仅导入需要的模块
import { debounce } from 'lodash-es';

1.3 图片优化
loader：
image-webpack-loader：压缩 JPEG/PNG/WebP。
url-loader：小图片转为 base64（减少 HTTP 请求）。
配置：
javascript
{
  test: /\.(png|jpg|gif)$/,
  use: [
    {
      loader: 'url-loader',
      options: { limit: 8192 } // 小于 8KB 的图片转为 base64
    },
    'image-webpack-loader' // 压缩图片
  ]
}

2. 加载速度优化
2.1 代码分割与懒加载
策略：
路由懒加载：
javascript
// React 示例
const Home = React.lazy(() => import('./Home'));

<React.Suspense fallback={<div>Loading...</div>}>
  <Route path="/home" component={Home} />
</React.Suspense>

动态导入：按需加载非首屏资源（如大组件、图表库）。
SplitChunksPlugin：提取公共依赖为单独 chunk。
javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors'
        }
      }
    }
  }
};

2.2 资源预加载 / 预取
插件：html-webpack-preload-plugin。
配置：
javascript
new HtmlWebpackPreloadPlugin({
  rel: 'preload',
  as: 'script',
  include: 'asyncChunks' // 预加载异步 chunk
});

效果：通过 <link rel="preload"> 提前加载关键资源。
2.3 CSS 提取与分割
插件：MiniCssExtractPlugin。
配置：
javascript
{
  test: /\.css$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader']
},
new MiniCssExtractPlugin({
  filename: '[name].[contenthash].css'
})

优势：CSS 与 JS 并行加载，避免阻塞渲染。
3. 运行时性能优化
3.1 长效缓存
文件名哈希：
javascript
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].chunk.js'
}

效果：内容不变时哈希值不变，利用浏览器缓存。
3.2 懒加载 Polyfill
策略：按需加载浏览器兼容性代码。
示例：
javascript
if (!('Intl' in window)) {
  import('intl'); // 仅在需要时加载 Intl Polyfill
}

3.3 DLLPlugin 预编译
适用场景：大型第三方库（如 React、Vue）。
配置步骤：
创建 DLL 配置文件：
javascript
// webpack.dll.js
module.exports = {
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.js',
    library: '[name]_[hash]'
  }
};

在主配置中引用：
javascript
new webpack.DllReferencePlugin({
  manifest: require('./vendor-manifest.json')
})

效果：第三方库仅需编译一次，大幅提升构建速度。
4. 其他性能优化技巧
4.1 分析与监控
工具：
webpack-bundle-analyzer：可视化分析 bundle 组成。
speed-measure-webpack-plugin：测量各 loader/plugin 耗时。
配置：
javascript
new BundleAnalyzerPlugin({ analyzerMode: 'static' });

4.2 生产环境移除调试代码
插件：
DefinePlugin：定义环境变量。
terser-webpack-plugin：移除 console 和 debugger。
配置：
javascript
new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('production')
}),
new TerserPlugin({
  terserOptions: { compress: { drop_console: true } }
})

4.3 压缩 HTML
插件：HtmlWebpackPlugin 内置压缩。
配置：
javascript
new HtmlWebpackPlugin({
  minify: {
    collapseWhitespace: true,
    removeComments: true
  }
})


总结优化清单
体积优化：压缩代码、Tree Shaking、图片优化。
加载优化：代码分割、懒加载、预加载、CSS 提取。
缓存优化：长效哈希、DLLPlugin。
运行时优化：移除调试代码、按需加载 Polyfill。
通过 Webpack 全方位优化，可显著减少首屏加载时间（TTFB）、提升交互响应速度，并降低用户流量消耗。`
              }
            ]
          },
          {
            question: `10.与webpack类似的工具还有哪些？区别？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `前端构建工具对比：Webpack、Rollup、Vite、Parcel
核心工具概览
工具	定位	优势场景	代表项目
Webpack	通用打包工具	复杂应用（SPA/MFE）	企业级 React/Vue 应用
Rollup	专注 ESModule 的打包工具	库 / 组件开发	React、Vue 源码
Vite	基于 ESBuild 的极速构建工具	开发环境（HMR 毫秒级响应）	中大型前端项目
Parcel	零配置打包工具	小型项目 / 快速原型	个人博客、Demo 应用
1. Webpack
特点：
万能加载器：通过 Loader 支持几乎所有资源类型（JS、CSS、图片等）。
强大的插件系统：覆盖从编译到优化的全生命周期（如 HMR、代码分割）。
复杂配置：需手动配置 Loader/Plugin，学习曲线陡峭。
适用场景：
包含多种资源类型的复杂应用。
需要自定义构建流程（如多环境、多页面应用）。
局限：
冷启动和热更新速度较慢（依赖完整编译）。
2. Rollup
特点：
ESModule 优先：原生支持 Tree Shaking，打包结果更精简。
专注库开发：生成 UMD/ESModule 等多种格式，适合开发 npm 包。
轻量配置：默认行为简单，插件生态不如 Webpack 丰富。
适用场景：
开发 JS 库、组件库（如 React、Lodash）。
对体积敏感的项目（如微前端子应用）。
局限：
对非 JS 资源支持较弱（需额外插件）。
缺乏 HMR、代码分割等高级功能。
3. Vite
特点：
极速冷启动：基于 ESBuild 预构建依赖，开发环境无需打包。
按需加载：使用浏览器原生 ESM 实现 HMR，响应速度毫秒级。
无缝迁移：兼容 Webpack 生态（可通过插件扩展）。
适用场景：
开发环境需要高频修改的项目。
中大型应用（尤其 Vue 3 项目，官方推荐）。
局限：
生产环境仍依赖 Rollup，复杂场景配置可能不如 Webpack 灵活。
4. Parcel
特点：
零配置：自动识别文件类型并应用默认处理（如 CSS 自动压缩）。
多线程加速：内部使用 worker 并行处理文件。
友好新手：无需理解复杂概念即可快速上手。
适用场景：
小型项目、个人开发、教学演示。
快速原型开发（如 Hackathon）。
局限：
配置灵活性低，难以满足复杂需求。
性能优化能力有限（如自定义代码分割）。
关键区别对比表
维度	Webpack	Rollup	Vite	Parcel
核心优势	功能全面、生态成熟	库打包、Tree Shaking 高效	开发体验极致、按需加载	零配置、开箱即用
构建速度	慢（全量编译）	中等（依赖规模）	极快（ESBuild + ESM）	中等（多线程）
配置复杂度	高（需手动配置 Loader/Plugin）	低（适合简单场景）	中等（兼容 Webpack 插件）	极低（自动推断）
适用场景	复杂应用、企业级项目	库 / 组件开发	中大型应用开发	小型项目、快速原型
资源处理	支持所有资源类型	侧重 JS/TS	原生支持 + 插件扩展	自动处理常见类型
社区活跃度	极高（GitHub 120k+ Star）	高（GitHub 20k+ Star）	高（GitHub 40k+ Star）	中（GitHub 14k+ Star）
如何选择？
选 Webpack：
项目需要复杂的自定义构建流程（如多环境配置、自定义插件）。
使用非标准资源类型（如自定义文件格式）。
选 Rollup：
开发库或组件（需最小化体积）。
项目完全基于 ESModule，无需复杂资源处理。
选 Vite：
开发环境需要极致响应速度（如团队规模大、代码频繁修改）。
使用 Vue 3 或 React 18（官方推荐）。
选 Parcel：
快速启动小型项目，无需关注构建细节。
教学或个人项目，追求简单易用。
趋势与未来
Webpack：仍是企业级项目的主流选择，生态成熟度无可替代。
Vite：凭借开发体验优势，逐渐成为新项目首选（尤其 Vue/React 生态）。
ESBuild：作为底层编译工具，被越来越多的构建工具集成（如 Vite、Rollup）。
理解工具的核心差异，结合项目需求选择合适的构建方案，是提升开发效率的关键。`
              }
            ]
          },
        ]
      },
      {
        tag: `设计模式面试题`,
        data: [
          {
            question: `1.说说对设计模式的理解？常见的设计模式有哪些？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `对设计模式的理解
设计模式是解决软件设计中重复出现问题的通用解决方案，是前人总结的代码设计经验。它不是具体的代码实现，而是一套抽象的设计思想，目的是：
提高代码复用性：减少重复代码，降低维护成本。
增强可扩展性：便于添加新功能，符合 “开闭原则”（对扩展开放，对修改关闭）。
提升可读性：统一设计风格，让其他开发者更容易理解代码逻辑。
解耦模块依赖：降低组件间的耦合度，使系统更灵活。
常见的设计模式分类及示例
设计模式通常分为三大类：创建型模式、结构型模式、行为型模式。以下是前端常用的设计模式：
一、创建型模式（关注对象创建方式）
负责对象的创建过程，隐藏创建逻辑，使代码更灵活。
单例模式（Singleton）
核心：保证一个类只有一个实例，并提供全局访问点。
应用场景：全局状态管理（如 Vuex 的 store）、弹窗组件（避免重复创建多个弹窗实例）。
示例：
javascript
class Singleton {
  constructor() {
    if (Singleton.instance) return Singleton.instance;
    Singleton.instance = this;
  }
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

工厂模式（Factory）
核心：通过工厂类统一创建不同类型的对象，隐藏创建细节。
应用场景：根据条件生成不同组件（如表单中的输入框、下拉框）。
示例：
javascript
class ButtonFactory {
  createButton(type) {
    switch (type) {
      case 'primary': return new PrimaryButton();
      case 'success': return new SuccessButton();
      default: throw new Error('未知按钮类型');
    }
  }
}

构造器模式（Constructor）
核心：通过构造函数初始化对象，统一属性和方法。
应用场景：创建具有相同结构的对象（如用户信息、商品数据）。
示例：
javascript
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHi() { /* ... */ }
}
const user = new User('张三', 20);

二、结构型模式（关注对象组合与结构）
处理类或对象的组合，实现更灵活的结构。
代理模式（Proxy）
核心：为对象提供一个代理层，控制对原对象的访问（如拦截、增强）。
应用场景：数据校验、缓存、权限控制（如 Vue 3 的响应式原理基于 Proxy）。
示例：
javascript
const proxy = new Proxy(target, {
  get(target, key) {
    console.log(’访问了 ￥{ key }‘);
    return target[key];
  }
});

装饰器模式（Decorator）
核心：动态给对象添加新功能，不改变原对象结构。
应用场景：日志记录、性能监控（如 React 的 HOC 高阶组件）。
示例：
javascript
function withLog(fn) {
  return function(...args) {
    console.log('执行前日志');
    const result = fn(...args);
    console.log('执行后日志');
    return result;
  };
}
const add = (a, b) => a + b;
const addWithLog = withLog(add);

适配器模式（Adapter）
核心：将一个类的接口转换为另一个接口，使不兼容的类可以一起工作。
应用场景：适配不同 API 格式（如将后端返回的字段映射为前端所需格式）。
示例：
javascript
// 后端数据格式：{ user_name: '张三' }
// 前端需要：{ name: '张三' }
function adaptUser(rawUser) {
  return { name: rawUser.user_name };
}

组合模式（Composite）
核心：将对象组合成树形结构，统一处理单个对象和组合对象。
应用场景：树形组件（如文件夹结构、菜单层级）。
示例：
javascript
class Node {
  constructor(name) {
    this.name = name;
    this.children = [];
  }
  add(child) { this.children.push(child); }
  render() { /* 递归渲染自身和子节点 */ }
}

三、行为型模式（关注对象间的交互）
描述对象如何协作完成任务，定义对象间的通信方式。
观察者模式（Observer）
核心：一个对象（被观察者）状态变化时，自动通知所有依赖它的对象（观察者）。
应用场景：事件监听（如 DOM 事件）、状态管理（如 Vue 的响应式更新）。
示例：
javascript
class Subject {
  constructor() { this.observers = []; }
  addObserver(observer) { this.observers.push(observer); }
  notify() { this.observers.forEach(o => o.update()); }
}
class Observer { update() { /* 处理更新 */ } }

发布 - 订阅模式（Pub/Sub）
核心：通过中间层（事件总线）连接发布者和订阅者，解耦两者关系。
应用场景：跨组件通信（如 EventBus）、全局事件管理。
示例：
javascript
class EventBus {
  constructor() { this.events = {}; }
  on(type, callback) {
    this.events[type] = this.events[type] || [];
    this.events[type].push(callback);
  }
  emit(type, data) {
    this.events[type]?.forEach(cb => cb(data));
  }
}

策略模式（Strategy）
核心：定义一系列算法，封装每个算法，并使它们可互换。
应用场景：多种相似逻辑的选择（如表单验证规则、支付方式选择）。
示例：
javascript
const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};
function calculate(strategy, a, b) {
  return strategies[strategy](a, b);
}

迭代器模式（Iterator）
核心：提供一种遍历集合元素的统一接口，无需暴露集合内部结构。
应用场景：数组 forEach、map 方法，Generator 函数。
示例：
javascript
function createIterator(arr) {
  let index = 0;
  return {
    next: () => ({
      value: arr[index],
      done: index++ >= arr.length
    })
  };
}


总结
设计模式的核心是解决特定场景的问题，而非教条式的套用。前端常用模式包括：
单例模式（全局状态）、观察者模式（事件通信）、代理模式（数据拦截）、策略模式（逻辑复用）等。
理解设计模式的关键是掌握其解决的问题和适用场景，在实际开发中灵活运用，避免过度设计。`
              }
            ]
          },
          {
            question: `2.说说你对工厂模式的理解？应用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `对工厂模式的理解及应用场景
在前端开发中，工厂模式是一种创建型设计模式，其核心思想是通过一个专门的 “工厂” 函数来封装对象的创建过程，而非直接使用 new 关键字实例化对象。这样做的目的是将对象的创建逻辑与使用逻辑分离，提高代码的灵活性、可维护性和可扩展性。
1. 工厂模式的核心思想
隐藏创建细节：调用者无需知道对象的具体创建步骤，只需调用工厂函数并传入必要参数，即可获取所需对象。
统一接口：工厂函数返回的对象通常遵循相同的接口（或具有相似的方法 / 属性），方便调用者统一处理。
动态创建：可根据传入的参数动态决定创建哪种类型的对象，无需硬编码具体类型。
2. 工厂模式的常见类型
在前端中，工厂模式可分为以下几类（从简单到复杂）：
简单工厂模式（最常用）
由一个工厂函数根据参数直接创建并返回不同类型的对象。
示例：
javascript
// 定义不同类型的对象构造函数
function Button(text) {
  this.text = text;
  this.render = () => console.log(’渲染按钮：￥{ this.text }‘);
}

function Input(placeholder) {
  this.placeholder = placeholder;
  this.render = () => console.log(’渲染输入框：￥{ this.placeholder }‘);
}

// 工厂函数：根据类型创建不同组件
function createComponent(type, options) {
  switch(type) {
    case 'button':
      return new Button(options.text);
    case 'input':
      return new Input(options.placeholder);
    default:
      throw new Error('未知组件类型');
  }
}

// 使用工厂创建对象
const btn = createComponent('button', { text: '提交' });
const input = createComponent('input', { placeholder: '请输入' });
btn.render(); // 渲染按钮：提交
input.render(); // 渲染输入框：请输入

工厂方法模式
定义一个创建对象的接口（抽象工厂），由子类决定具体创建哪种对象。前端中较少直接使用（因 JavaScript 无原生抽象类），但可模拟实现。
抽象工厂模式
用于创建一系列相关或相互依赖的对象族（如不同主题的组件库：按钮、输入框、下拉框等）。适合复杂场景，前端中多用于大型 UI 组件库设计。
3. 前端应用场景
工厂模式在前端开发中应用广泛，典型场景包括：
UI 组件创建
如封装一个组件工厂，根据参数创建不同类型的表单组件（按钮、输入框、单选框等），尤其在动态表单生成中常用。
数据模型实例化
当后端返回不同类型的数据（如用户、商品、订单）时，可通过工厂函数统一转换为对应的数据模型对象，便于后续处理（如添加统一的格式化方法）。
示例：
javascript
function UserModel(data) { /* 用户数据处理逻辑 */ }
function ProductModel(data) { /* 商品数据处理逻辑 */ }

function createModel(type, data) {
  switch(type) {
    case 'user': return new UserModel(data);
    case 'product': return new ProductModel(data);
    default: return null;
  }
}

// 处理接口返回数据
const user = createModel('user', apiData.user);
const product = createModel('product', apiData.product);

工具类 / 服务创建
如创建不同的日志服务（控制台日志、远程日志），根据环境（开发 / 生产）通过工厂函数切换实现。
避免重复代码
当多个地方需要创建结构相似的对象时，工厂模式可集中管理创建逻辑，减少重复代码（符合 DRY 原则）。
4. 优点与注意事项
优点：
降低耦合：对象创建与使用分离，修改创建逻辑无需改动调用处。
便于扩展：新增对象类型时，只需扩展工厂函数，无需修改现有代码（符合开放 - 封闭原则）。
注意事项：
过度使用会增加代码复杂度（简单场景直接 new 可能更清晰）。
工厂函数逻辑不宜过于复杂，否则会导致维护困难。
总之，工厂模式在前端中是一种实用的设计模式，尤其适合需要动态创建多种相似对象的场景，能有效提升代码的灵活性和可维护性。`
              }
            ]
          },
          {
            question: `3.说说你对单例模式的理解？如何实现?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `单例模式的理解与实现
单例模式是一种创建型设计模式，其核心思想是确保一个类只有一个实例，并提供一个全局访问点来获取该实例。在前端开发中，单例模式常用于管理全局状态、资源共享或避免重复创建相同对象。
1. 单例模式的核心特性
唯一性：无论调用多少次创建方法，返回的都是同一个实例。
全局访问：提供一个全局可访问的静态方法（如 getInstance()）获取该实例。
延迟实例化：实例在第一次被请求时才创建，而非在程序启动时就初始化。
2. 常见实现方式
方式一：ES6 类 + 静态属性
javascript
class Singleton {
  constructor() {
    // 如果实例已存在，则直接返回
    if (Singleton.instance) {
      return Singleton.instance;
    }
    // 初始化操作
    this.data = [];
    // 保存实例
    Singleton.instance = this;
  }

  // 静态方法：获取实例
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }

  // 其他方法
  addItem(item) {
    this.data.push(item);
  }
}

// 使用示例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
方式二：立即执行函数（闭包）
javascript
const Singleton = (function() {
  let instance;

  function createInstance() {
    // 私有构造函数
    const obj = {
      data: [],
      addItem(item) {
        this.data.push(item);
      }
    };
    return obj;
  }

  return {
    // 公共方法：获取实例
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// 使用示例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
方式三：ES6 模块（最简实现）
javascript
// singleton.js
class Singleton {
  constructor() {
    this.data = [];
  }
  addItem(item) {
    this.data.push(item);
  }
}

// 直接导出实例（模块在首次导入时执行，且仅执行一次）
export default new Singleton();

// 使用示例
import singleton from './singleton.js';
import anotherSingleton from './singleton.js';
console.log(singleton === anotherSingleton); // true
3. 前端应用场景
全局状态管理：如 Redux 的 store、Vuex 的 store，确保整个应用中只有一个状态容器。
浏览器 API 封装：如封装 localStorage、WebSocket 连接，避免重复创建。
javascript
// WebSocket 单例
class WebSocketClient {
  constructor(url) {
    if (WebSocketClient.instance) {
      return WebSocketClient.instance;
    }
    this.socket = new WebSocket(url);
    WebSocketClient.instance = this;
  }
  // 其他方法...
}
配置对象：应用的全局配置，确保所有模块使用同一配置实例。
4. 优缺点
优点：
节省内存：避免重复创建相同对象。
简化全局访问：无需传递实例，直接通过静态方法获取。
缺点：
违反单一职责原则：类既负责业务逻辑，又负责管理实例生命周期。
难以测试：单例状态可能影响测试的独立性。
多线程环境需额外处理：JavaScript 是单线程，但在 Node.js 多进程环境中需注意。
5. 注意事项
延迟实例化：确保实例在真正需要时才创建，避免资源浪费。
防止外部实例化：构造函数应设为私有或通过逻辑限制（如上述示例）。
模块化导出：在 ES6 中，推荐使用模块导出单例（方式三），代码更简洁。
单例模式是前端开发中常用的设计模式，适合需要全局唯一实例的场景，但需谨慎使用，避免过度设计。`
              }
            ]
          },
          {
            question: `4.说说你对策略模式的理解？应用场景?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `策略模式的理解与应用场景
策略模式是一种行为型设计模式，其核心思想是将一组相似的算法封装成独立的策略类，并使它们可以互相替换。通过这种方式，算法的变化不会影响使用算法的客户端，从而实现解耦和可扩展性。
1. 策略模式的核心结构
策略接口 / 抽象类：定义所有具体策略必须实现的公共方法（如计算、验证等）。
具体策略类：实现策略接口，封装具体的算法逻辑。
上下文类：持有一个策略对象的引用，负责根据需要选择和调用相应的策略。
2. 简单示例：表单验证策略
假设需要对表单输入进行多种验证（如必填、邮箱格式、长度限制），使用策略模式实现：
javascript
// 1. 定义策略接口（验证方法）
const strategies = {
  isNotEmpty(value, errorMsg) {
    return value.trim() === '' ? errorMsg : null;
  },
  isEmail(value, errorMsg) {
    return !/^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]+$/.test(value) ? errorMsg : null;
  },
  minLength(value, length, errorMsg) {
    return value.length < length ? errorMsg : null;
  }
};

// 2. 上下文类：验证器
class Validator {
  constructor() {
    this.rules = [];
  }

  // 添加验证规则
  add(field, strategyName, errorMsg, ...args) {
    this.rules.push(() => {
      return strategies[strategyName](field, ...args, errorMsg);
    });
    return this; // 链式调用
  }

  // 执行验证
  validate() {
    for (const rule of this.rules) {
      const errorMsg = rule();
      if (errorMsg) return errorMsg;
    }
    return null; // 验证通过
  }
}

// 3. 使用示例
const validator = new Validator();
validator
  .add('username', 'isNotEmpty', '用户名不能为空')
  .add('email', 'isEmail', '邮箱格式不正确')
  .add('password', 'minLength', '密码长度不能少于6位', 6);

const error = validator.validate();
console.log(error); // 若有错误返回错误信息，否则返回null
3. 前端应用场景
策略模式在前端中广泛应用于以下场景：
1. 表单验证
如上述示例，将不同验证规则封装为独立策略，可动态组合和替换。
2. 动画 / 过渡效果
封装多种动画策略（如线性、缓入、缓出），在不同场景下切换。
javascript
const animationStrategies = {
  linear(t) { return t; },
  easeIn(t) { return t * t; },
  easeOut(t) { return t * (2 - t); }
};

class Animator {
  constructor(strategy) {
    this.strategy = strategy;
  }
  animate(t) {
    return this.strategy(t);
  }
}
3. 支付方式选择
根据用户选择的支付方式（微信、支付宝、银行卡），执行不同的支付逻辑。
javascript
const paymentStrategies = {
  wechatPay(order) { /* 微信支付逻辑 */ },
  alipay(order) { /* 支付宝支付逻辑 */ },
  bankCard(order) { /* 银行卡支付逻辑 */ }
};

class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }
  pay(order) {
    return this.strategy(order);
  }
}
4. 数据格式化
根据不同需求（如显示金额、日期、百分比），使用不同的格式化策略。
javascript
const formatStrategies = {
  currency(value) { return ’¥￥{ value.toFixed(2) }‘; },
  date(value) { return new Date(value).toLocaleDateString(); },
  percent(value) { return ’￥{(value * 100).toFixed(1)} %‘; }
};
4. 策略模式的优点
可扩展性：新增策略时无需修改原有代码，符合开闭原则。
解耦算法：将算法逻辑与客户端分离，提高代码复用性。
清晰结构：避免大量条件判断（如 if-else 或 switch），使代码更易维护。
5. 与其他模式的对比
策略模式 vs 状态模式：
策略模式：由客户端主动选择并切换策略。
状态模式：状态自动触发相应行为，客户端无需关心状态变化。
策略模式 vs 工厂模式：
策略模式：关注算法的替换和执行。
工厂模式：关注对象的创建。
6. 注意事项
策略类数量：若策略过多，可能导致类爆炸，可结合工厂模式管理策略实例。
简单场景简化：对于简单场景，可直接使用对象字面量（如上述验证示例），无需创建复杂的类结构。
策略模式是前端开发中解决多种条件分支逻辑的有效方式，尤其适合需要动态切换算法或行为的场景。通过将变化封装在独立的策略类中，可以显著提高代码的灵活性和可维护性。`
              }
            ]
          },
          {
            question: `5.说说你对发布订阅、观察者模式的理解？区别?`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `发布订阅与观察者模式的理解及区别
发布订阅模式（Pub/Sub）和观察者模式（Observer）是两种行为型设计模式，核心都是实现对象间的一对多依赖关系，当一个对象状态变化时，所有依赖它的对象都会得到通知。但它们的实现方式和应用场景存在细微差异。
1. 观察者模式（Observer）
核心思想：
一个被观察对象（Subject）维护一组观察者（Observer），当被观察对象状态变化时，直接调用所有观察者的更新方法。
Subject 与 Observer 直接耦合，Observer 需要注册到 Subject 中。
结构：
Subject：维护观察者列表，提供注册 / 注销 / 通知方法。
Observer：定义更新接口，当收到通知时执行相应操作。
示例：
javascript
// 被观察对象
class Subject {
  constructor() {
    this.observers = [];
  }
  // 注册观察者
  register(observer) {
    this.observers.push(observer);
  }
  // 注销观察者
  unregister(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }
  // 通知所有观察者
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

// 观察者
class Observer {
  update(data) {
    console.log(’收到更新：￥{ data }‘);
  }
}

// 使用示例
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.register(observer1);
subject.register(observer2);
subject.notify("状态更新"); // 两个观察者都会收到通知

2. 发布订阅模式（Pub/Sub）
核心思想：
通过一个 ** 事件总线（Event Bus/Mediator）** 作为中间层，** 发布者（Publisher）** 将事件发布到总线，** 订阅者（Subscriber）** 从总线订阅感兴趣的事件。
发布者与订阅者完全解耦，双方无需知道对方的存在。
结构：
Event Bus：维护事件注册表，提供发布 / 订阅 / 取消订阅方法。
Publisher：发布事件到总线，不关心谁订阅。
Subscriber：从总线订阅事件，不关心谁发布。
示例：
javascript
// 事件总线
class EventBus {
  constructor() {
    this.events = {};
  }
  // 订阅事件
  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
  // 发布事件
  publish(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(data));
    }
  }
  // 取消订阅
  unsubscribe(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        cb => cb !== callback
      );
    }
  }
}

// 使用示例
const eventBus = new EventBus();

// 订阅者
const subscriber1 = (data) => console.log(’订阅者1收到：￥{ data }‘);
const subscriber2 = (data) => console.log(‘订阅者2收到：￥{ data }’);

eventBus.subscribe("message", subscriber1);
eventBus.subscribe("message", subscriber2);

// 发布者
eventBus.publish("message", "Hello World"); // 两个订阅者都会收到

3. 核心区别对比
维度	观察者模式	发布订阅模式
耦合度	被观察对象与观察者直接耦合	发布者与订阅者完全解耦，通过中间层通信
通信方式	被观察对象直接调用观察者的更新方法	通过事件总线间接通信
事件管理	被观察对象内部维护观察者列表	事件总线集中管理事件和回调
适用场景	简单的一对多依赖，如状态管理	复杂系统中的跨模块通信，如大型应用
典型实现	Vue 的响应式系统、DOM 事件监听	消息队列、Redux 的 store.dispatch
4. 前端应用场景
观察者模式
Vue 的响应式系统：数据变化时直接通知依赖的 DOM 更新。
React 的 useEffect：依赖项变化时触发回调。
DOM 事件监听：addEventListener 本质是注册观察者。
发布订阅模式
全局事件总线（Event Bus）：跨组件通信（如 Vue 的 $emit/$on）。
WebSocket 消息处理：服务端消息通过总线分发给不同模块。
微服务间通信：通过消息队列（如 RabbitMQ）实现异步通信。
5. 总结
观察者模式适合简单场景，强调对象间的直接依赖关系，实现更简洁。
发布订阅模式适合复杂系统，通过中间层解耦，扩展性更强，但引入额外复杂度。
在前端开发中，两者常结合使用（如 Redux 既包含观察者模式的思想，也使用发布订阅模式的事件机制）。理解它们的核心差异，有助于在不同场景下选择更合适的设计模式。`
              }
            ]
          },
          {
            question: `6.说说你对代理模式的理解？应用场景？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `对代理模式的理解及应用场景
代理模式（Proxy Pattern）是一种结构型设计模式，其核心思想是：为一个对象提供一个代理（Proxy），通过代理来控制对原对象的访问。代理对象可以在调用原对象方法前后添加额外逻辑，实现如权限控制、缓存、日志记录等功能，同时不改变原对象的核心逻辑。
1. 代理模式的核心角色
目标对象（Target）：被代理的原始对象，包含核心业务逻辑。
代理对象（Proxy）：与目标对象实现相同的接口（或拥有相似的方法），负责转发请求给目标对象，并在转发前后添加额外操作。
客户端（Client）：通过代理对象访问目标对象，无需直接与目标对象交互。
2. 代理模式的实现逻辑
代理模式的核心是 **“控制访问”**，常见的代理逻辑包括：
前置处理：如参数校验、权限判断、缓存查询等。
转发请求：将客户端的请求传递给目标对象。
后置处理：如结果缓存、日志记录、性能统计等。
示例（JavaScript 实现）：
假设需要对一个图片加载函数添加 “预加载占位图” 和 “加载日志” 功能，可用代理模式实现：
javascript
// 目标对象：加载图片
class ImageLoader {
  load(url) {
    console.log(’正在加载图片：￥{ url }‘);
    // 模拟图片加载
    return new Promise(resolve => {
      setTimeout(() => resolve(‘图片 ￥{ url } 加载完成’), 1000);
    });
  }
}

// 代理对象：控制对 ImageLoader 的访问
class ImageProxy {
  constructor(target) {
    this.target = target; // 保存目标对象
    this.cache = new Map(); // 缓存已加载的图片
  }

  async load(url) {
    // 前置处理：检查缓存
    if (this.cache.has(url)) {
      console.log(’使用缓存：￥{ url }‘);
      return this.cache.get(url);
    }

    // 前置处理：显示占位图
    console.log(‘显示占位图...’);

    // 转发请求给目标对象
    const result = await this.target.load(url);

    // 后置处理：缓存结果 + 记录日志
    this.cache.set(url, result);
    console.log(’记录日志：￥{ result }‘);

    return result;
  }
}

// 客户端使用（只与代理交互）
const imageLoader = new ImageLoader();
const proxy = new ImageProxy(imageLoader);

proxy.load("pic1.jpg"); 
// 输出：
// 显示占位图...
// 正在加载图片：pic1.jpg
// 记录日志：图片 pic1.jpg 加载完成

proxy.load("pic1.jpg"); 
// 输出：
// 使用缓存：pic1.jpg（直接走缓存，不重复加载）
3. 代理模式的分类（按功能）
根据代理的作用，常见分类包括：
保护代理：控制对目标对象的访问权限（如验证用户是否有权限调用某接口）。
缓存代理：对目标对象的结果进行缓存，避免重复计算或请求（如示例中的图片缓存）。
虚拟代理：延迟初始化目标对象，在真正需要时才创建（如大型图片懒加载）。
日志代理：记录目标对象的调用日志（如接口请求参数、返回值、耗时）。
防抖 / 节流代理：对高频调用的方法进行防抖或节流处理（如搜索输入框的联想请求）。
4. 前端应用场景
代理模式在前端开发中应用广泛，典型场景包括：
（1）Vue 的响应式系统（基于 Proxy）
Vue 3 中使用 Proxy 对象代理数据对象，当数据被读取或修改时，代理会触发依赖收集（get 拦截）或视图更新（set 拦截），这是代理模式的经典应用：
javascript
const data = { name: "Vue" };
const proxyData = new Proxy(data, {
  get(target, key) {
    // 前置处理：收集依赖
    track(target, key);
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    // 后置处理：触发更新
    trigger(target, key);
  }
});
（2）API 请求代理（如 Axios 拦截器）
Axios 的拦截器本质是代理模式：请求发出前通过 request 拦截器添加 token、设置请求头；响应返回后通过 response 拦截器处理错误、解析数据：
javascript
axios.interceptors.request.use(config => {
  // 前置处理：添加 token
  config.headers.token = "xxx";
  return config;
});

axios.interceptors.response.use(response => {
  // 后置处理：返回数据
  return response.data;
}, error => {
  // 错误处理
  console.error("请求失败：", error);
});
（3）图片懒加载（虚拟代理）
对于大量图片的页面，通过代理对象控制图片加载时机（滚动到可视区域时才真正加载），减少初始加载时间：
javascript
class LazyImageProxy {
  constructor(imgDom, realSrc) {
    this.imgDom = imgDom;
    this.realSrc = realSrc;
    this.bindScroll(); // 监听滚动事件
  }

  bindScroll() {
    window.addEventListener("scroll", () => {
      if (this.isInViewport()) {
        // 进入可视区域，加载真实图片
        this.imgDom.src = this.realSrc;
        // 移除监听（避免重复执行）
        window.removeEventListener("scroll", this.bindScroll);
      }
    });
  }

  isInViewport() {
    // 判断图片是否在可视区域内
    const rect = this.imgDom.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom > 0;
  }
}
（4）权限控制（保护代理）
在调用敏感操作（如删除数据）前，通过代理验证用户权限，无权限则阻止操作：
javascript
class DeleteProxy {
  constructor(target, userRole) {
    this.target = target; // 目标对象（包含真正的删除方法）
    this.userRole = userRole; // 用户角色
  }

  delete(id) {
    if (this.userRole !== "admin") {
      console.log("权限不足，无法删除");
      return;
    }
    // 权限通过，转发请求
    this.target.delete(id);
  }
}
5. 总结
代理模式的核心价值是 **“控制访问 + 功能增强”，它通过引入代理对象，在不修改原对象的前提下，灵活添加额外逻辑，符合开放封闭原则 **（对扩展开放，对修改封闭）。
在前端开发中，从数据响应式、API 拦截到图片懒加载、权限控制，代理模式无处不在。掌握它能帮助我们写出更灵活、可维护的代码。`
              }
            ]
          },
        ]
      },
      {
        tag: `常见的场景题`,
        data: [
          {
            question: `1. 前端如何实现截图？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `前端实现截图需要使⽤HTML5的Canvas和相关API，具体步骤如下：
1. ⾸先在⻚⾯中创建⼀个Canvas元素，并设置其宽⾼和样式。
2. 使⽤CanvasAPI在Canvas上绘制需要截图的内容，⽐如⻚⾯的某个区域、某个元素、图⽚等。
3. 调⽤CanvasAPI中的
toDataURL() 
⽅法将Canvas转化为base64编码的图⽚数据。
4. 将base64编码的图⽚数据传递给后端进⾏处理或者直接在前端进⾏显⽰。
以下是⼀个简单的例⼦，实现了对整个⻚⾯的截图：
 《!DOCTYPE 
html>《html<head<meta charset="UTF-8"<title
截图⽰例
</title<style
 #c
 anvas {position: fixed;left: 0;top: 0;z-index: 9999;}
 </style</head<body<h1
截图⽰例
</h1<p
这是⼀个简单的截图⽰例。
</p<button id="btn"
截
图
</button<canvas《script id="canvas"</canvas《script
 const canvas = document.getElementById('canvas');const ctx = 
canvas.getContext('2d');const btn = document.getElementById('btn');
 canvas.width = window.innerWidth;
 canvas.height = window.innerHeight;
 btn.addEventListener('click', () => {
 ctx.clearRect(0, 0, canvas.width, canvas.height);
 ctx.drawImage(document.documentElement, 0, 0);const imgData = 
canvas.toDataURL();console.log(imgData);
 });《/script< /body《/html canvas 元素，并设置其宽⾼和样式，将其放在⻚⾯最上⽅。在 点击“截图”按钮时，通过 这个例⼦中，在⻚⾯中创建了⼀个 toDataURL()
  ⽅法将整个⻚⾯的截图转换为base64编码的图⽚数据， 并打印到控制台上。`
              }
            ]
          },
          {
            question: `2. 当QPS达到峰值时,该如何处理？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `当QPS达到峰值时，可以从以下⼏个⽅⾯来进⾏优化：
1. 数据库优化：数据库的优化包括优化SQL语句、使⽤索引、避免全表扫描、分表分库等措施，以提
⾼数据库的读写性能。
2. 缓存优化：缓存可以降低对数据库的访问频率，提⾼响应速度。可以使⽤Redis、Memcached等缓
存技术，减轻服务器负载。
3. 代码优化：优化代码可以提⾼代码的执⾏效率，减少不必要的开销。可以通过⼀些优化⼿段，如减
少不必要的代码执⾏、避免循环嵌套、避免不必要的递归调⽤等来提⾼代码的性能。
4. 负载均衡：负载均衡可以将请求分发到多个服务器上，减少单个服务器的负载，提⾼整个系统的性
能和可⽤性。
5. 异步处理：将⼀些计算量⼤、耗时⻓的操作异步处理，减少对主线程的阻塞，提⾼响应速度。
6. CDN加速：使⽤CDN技术可以将静态资源缓存到CDN节点上，提⾼资源的加载速度，减少服务器的
负载。
7. 硬件升级：可以通过升级服务器硬件，增加带宽等⽅式来提⾼系统的处理能⼒。
以上是⼀些常⻅的优化⼿段，需要根据具体情况进⾏选择和实施。`
              }
            ]
          },
          {
            question: `3. js 超过Number最⼤值的数怎么处理？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `js 超过Number最⼤值的数怎么处理
在JavaScript 中，超过处理超过Number.MAX_VALUE 的数值被认为是Infinity （正⽆穷⼤）。如果要Number.MAX_VALUE 
的数值，可以使⽤第三⽅的JavaScript库，如bignumber.js，这些库可以处理任意精度的数值。
例如，使⽤big.js 库可以将两个超过Number.MAX_VALUE 的数相加：
 const big = require('big.js');
 const x = new big('9007199254740993');
 const y = new big('100000000000000000');
 const result = x.plus(y);
console.log(result.toString()); // 输出：100009007194925474093 
这⾥创建了两个big.js 对象x 和y，分别存储超过Number.MAX_VALUE 的数值。通过plus ⽅法将它们相加，得到了正确的结果。最后，通过toString ⽅法将结果转换为字符串。
如果不依赖外部库，咋处理
JavaScript 中，数值超过了Number最⼤值时，可以使⽤BigInt类型来处理，它可以表⽰任意精度的整数。
使⽤BigInt类型时，需要在数值后⾯添加⼀个n 后缀来表⽰BigInt类型。例如：
 const bigNum = 9007199254740993n; // 
注意：数字后⾯添加了 'n' 后缀
 
注意，BigInt类型是ECMAScript2020新增的特性，因此在某些浏览器中可能不被⽀持。如果需要在
不⽀持BigInt的环境中使⽤BigInt，可以使⽤polyfill或者第三⽅库来实现。`
              }
            ]
          },
          {
            question: `4. 使⽤同⼀个链接，如何实现PC打开是web应⽤、⼿机打开是⼀个H5应⽤？`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `可以通过根据请求来源（User-Agent）来判断访问设备的类型，然后在服务器端进⾏适配。例如，可
以在服务器端使⽤Node.js的Express框架，在路由中对不同的User-Agent进⾏判断，返回不同的⻚
⾯或数据。具体实现可以参考以下步骤：
1. 根据User-Agent判断访问设备的类型，例如判断是否为移动设备。可以使⽤第三⽅库如ua
parser-js 进⾏User-Agent 的解析。
2. 如果是移动设备，可以返回⼀个H5⻚⾯或接⼝数据。
3. 如果是PC设备，可以返回⼀个web应⽤⻚⾯或接⼝数据。
具体实现⽅式还取决于应⽤的具体场景和需求，以上只是⼀个⼤致的思路。`
              }
            ]
          },
          {
            question: `5. 如何保证⽤⼾的使⽤体验`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `【如何保证⽤⼾的使⽤体验】这个也是⼀个较为复杂的话题，这个也不是问题了，这个算是话题吧；
主要从以下⼏个⽅⾯思考问题：
1. 性能⽅向的思考
2. ⽤⼾线上问题反馈，线上oncall的思考
3. ⽤⼾使⽤体验的思考，交互体验使⽤⽅向
4. 提升⽤⼾能效⽅向思考`
              }
            ]
          },
          {
            question: `6. 如何解决⻚⾯请求接⼝⼤规模并发问题`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `如何解决⻚⾯请求接⼝⼤规模并发问题，不仅仅是包含了接⼝并发，还有前端资源下载的请求并发。
应该说这是⼀个话题讨论了；
个⼈认为可以从以下⼏个⽅⾯来考虑如何解决这个并发问题:
 1. 后端优化：可以对接⼝进⾏优化，采⽤缓存技术，对数据进⾏预处理，减少数据库操作等。使⽤集
群技术，将请求分散到不同的服务器上，提⾼并发量。另外可以使⽤反向代理、负载均衡等技术，
分担服务器压⼒。
2. 做BFF聚合：把所有⾸屏需要依赖的接⼝，利⽤服务中间层给聚合为⼀个接⼝。
3. CDN加速：使⽤CDN缓存技术可以有效减少服务器请求压⼒，提⾼⽹站访问速度。CDN缓存可以将
接⼝的数据存储在缓存服务器中，减少对原始服务器的访问，加速数据传输速度。
4. 使⽤WebSocket：使⽤WebSocket可以建⽴⼀个持久的连接，避免反复连接请求。WebSocket
可以实现双向通信，⼤幅降低服务器响应时间。
5. 使⽤HTTP2及其以上版本，使⽤多路复⽤。
6. 使⽤浏览器缓存技术：强缓存、协商缓存、离线缓存、ServiceWorker缓存等⽅向。
7. 聚合⼀定量的静态资源：⽐如提取⻚⾯公⽤复⽤部分代码打包到⼀个⽂件⾥⾯、对图⽚进⾏雪碧图
处理，多个图⽚只下载⼀个图⽚。
8. 采⽤微前端⼯程架构：只是对当前访问⻚⾯的静态资源进⾏下载，⽽不是下载整站静态资源。
9. 使⽤服务端渲染技术：从服务端把⻚⾯⾸屏直接渲染好返回，就可以避免掉⾸屏需要的数据再做
额外加载和执⾏。`
              }
            ]
          },
          {
            question: `7. 设计⼀套全站请求耗时统计⼯具`,
            description: ``,
            answer: [
              {
                text: `如果遇到了这道题就直接放弃吧，就说自己不知道`,
                daima: ``
              }
            ]
          },
          {
            question: `8. ⼤⽂件上传了解多少`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `⼤⽂件分⽚上传
如果太⼤的⽂件，⽐如⼀个视频1g2g那么⼤，直接采⽤上⾯的栗⼦中的⽅法上传可能会出链接现超时的情况，⽽且也会超过服务端允许上传⽂件的⼤⼩限制，所以解决这个问题我们可以将⽂件进⾏分⽚上传，每次只上传很⼩的⼀部分⽐如2M。
Blob 它表⽰原始数据,也就是⼆进制数据，同时提供了对数据截取的⽅法了Blob的功能，所以可以直接使⽤此⽅法对数据进⾏分段截图。
过程如下：
1. 把⼤⽂件进⾏分段⽐如2M，发送到服务器携带⼀个标志，暂时⽤当前的时间戳，⽤于标识⼀个完整的⽂件
2. 服务端保存各段⽂件
3. 浏览器端所有分⽚上传完成，发送给服务端⼀个合并⽂件的请求
4. 服务端根据⽂件标识、类型、各分⽚顺序进⾏⽂件合并
5. 删除分⽚⽂件
⼤⽂件上传断点续传
在上⾯我们实现了⽂件分⽚上传和最终的合并，现在要做的就是如何检测这些分⽚，不再重新上传即可。这⾥我们可以在本地进⾏保存已上传成功的分⽚，重新上传的时候使⽤hash，区分此⽂件是否已上传。
- 为每个分段⽣成hash值，使⽤spark-md5库
- 将上传成功的分段信息保存到本地
重新上传时，进⾏和本地分段hash值的对⽐，如果相同的话则跳过，继续下⼀个分段的上传
⽅案⼀：保存在本地 indexDB/localStorage等地⽅，推荐使⽤localforage这个库 npm install localforage
⽅案2：服务端⽤于保存分⽚坐标信息，返回给前端
需要服务端添加⼀个接⼝只是服务端需要增加⼀个接⼝。基于上⾯⼀个栗⼦进⾏改进，服务端已保存
了部分⽚段，客⼾端上传前需要从服务端获取已上传的分⽚信息（上⾯是保存在了本地浏览器），本
地对⽐每个分⽚的hash值，跳过已上传的部分，只传未上传的分⽚。
⽅法1是从本地获取分⽚信息,这⾥只需要将此⽅法的能⼒改为从服务端获取分⽚信息就⾏了。
`
              }
            ]
          },
          {
            question: `9. H5如何解决移动端适配问题`,
            description: ``,
            answer: [
              {
                text: ``,
                daima: `移动端适配问题是指如何让⽹⻚在不同的移动设备上显⽰效果相同。下⾯是⼀些常⻅的H5移动端适配⽅案：
                1. 使⽤viewport标签
通过设置viewport标签的meta属性，来控制⻚⾯的缩放⽐例和宽度，以适配不同的设备。例如：
 <meta name="viewport" content="width=device-width, initial-scale=1.0"
 其中 width=device-width表示设置viewport的宽度为设备宽度，initial-scale=1.0表示初始缩放比例为1.
 1. 使用css3的媒体查询
 通过css3的媒体查询，更具不同的设备的宽度设置不同的样式，以适配不同的设备，例如：
 arduinoCopy code@media screen and (max-width: 640px) {/*样式 */
}
其中max-width表示最大宽度，当屏幕宽度小于等于640px的时候，应用这些样式
1. 使用rem单位
通过将px转化为rem单位，根据不同的设备字体⼤⼩设置不同的样式，以适配不同的设备。例如：
html {font-size: 16px;
 }@
 media screen and (max-width: 640px) {html {font-size: 14px;}
 }
 div {width: 10rem;
 }
 其中font-size： 16px表示网页的基准字体大小设置为16px，font-size：14px表示在屏幕宽度小于等于640px的时候将基准字体大小设置为14px，div元素的width: 10rem表示元素的宽度为10个基准字体大小
 1. 使用flexible布局方案
 通过使用flexible布局方案，将px转换为rem单位，并且动态的根节点字体大小，以适配不同的设备，例如使用lib-flexible库
 arduinoCopy code// index.html
 《script src="https://cdn.bootcdn.net/ajax/libs/lib-flexible/0.3.4/flexible.js"
《/script
 // index.js
 import 'lib-flexible/flexible.js'
 其中，flexible.js会在页面加载的时候动态计算根节点的字体大小，并且将px转换为rem单位，在样式中可以直接使用px单位，例如：
  div {width: 100px;height: 100px;
 }
 这个div元素的⼤⼩会根据设备屏幕的宽度进⾏适配。
                `
              }
            ]
          },
          {
            question: `10. 站点⼀键换肤的实现⽅式有哪些？`,
            description: ``,
            answer: [
              {
                text: `⽹站⼀键换肤实现⽅式有以下⼏种`,
                daima: `1.  使⽤CSS变量：通过定义⼀些变量来控制颜⾊、字体等，然后在切换主题时动态修改这些变量的值。
                2. 使⽤class切换：在HTML的根元素上添加不同的class名称，每个class名称对应不同的主题样式，在切换主题时切换根元素的class名称即可。
                3. 使⽤JavaScript切换：使⽤JavaScript动态修改⻚⾯的样式，如修改元素的背景颜⾊、字体颜⾊等。
                4. 使⽤Less/Sass等CSS预处理器：通过预处理器提供的变量、函数等功能来实现主题切换。
                需要注意的是，⽆论采⽤哪种⽅式实现，都需要在设计⻚⾯样式时尽量遵循⼀些规范，如不使⽤绝对的像素值，使⽤相对单位等，以便更好地适应不同的屏幕⼤⼩和分辨率。
                以less举例，详细讲述⼀下具体操作流程
                通过Less实现⽹⻚换肤可以使⽤CSS变量和Less变量。CSS变量的语法如下：
                 :root {--primary-color: #007bff;
 }.btn {background-color: var(--primary-color);
 }
 ⽽Less变量则是通过Less预编译器提供的变量语法来实现的，如下所⽰：
  lessCopy code@primary-color: #007bff;
 .btn {background-color: @primary-color;
 }
 通过Less变量来实现⽹⻚换肤的⽅式可以在运⾏时使⽤JavaScript来修改Less变量的值，从⽽实现换肤效果。具体步骤如下：
 1. 使用less预编译器来编译less文件为css文件
 2. 在html文件中动态引入编译后的css文件
 3. 在js中动态修改less的变量的值
 4. 使用js将新的less变量的值注入到编译之后的文件中
 5. 将注入后的css样式应用到页面上
 以下是⼀段实现通过Less变量来实现⽹⻚换肤的⽰例代码：
  // base.less⽂件@primary-color: #007bff;
.btn {background-color: @primary-color;
 }
 // dark.less⽂件@primary-color: #343a40;
 <!-- index.html⽂件 -->
 《!DOCTYPE html>《html《head<meta charset="UTF-8"《title⽹⻚换肤⽰例《/title《link rel="stylesheet/less" type="text/css" href="base.less"《link
 rel="stylesheet/less" type="text/css" href="dark.less"《/head<body<button《script
class="btn"按钮</button《script src="less.min.js"《/script《script
 function changeSkin() {// 修改 Less 变量的值
less.modifyVars({'@primary-color': '#28a745'
 }).
 then(() => {console.log('换肤成功');
 }).
 catch(() => {console.error('换肤失败');
 });
 }《/script《/body《/html
 在上面的示例代码中，我们引入了两个less文件，一个是base.less文件，一个是dark.less文件，其中base.less中定义了一些基本的样式
 而dark.less则定义了一个暗黑色的主题样式。在js中我们使用less.modifyVars方法来秀嘎less的变量的值从而实现了换肤的效果，当然，这只是一个简单的示例代码
 实际的换肤功能还需要根据实际的需求来进行设计和实现。
`
              }
            ]
          }
        ]
      }
    ];

    // 生成导航栏
    function generateNav() {
      const desktopNav = document.getElementById('desktop-nav');
      const mobileNav = document.getElementById('mobile-nav');

      // 清空现有导航栏
      desktopNav.innerHTML = '';
      mobileNav.innerHTML = '';

      // 添加"全部题目"按钮
      const allBtnDesktop = document.createElement('button');
      allBtnDesktop.id = 'btn-all';
      allBtnDesktop.className = 'category-btn px-4 py-2 rounded-full text-sm font-medium transition-all hover:bg-primary/10 hover:text-primary';
      allBtnDesktop.textContent = '全部题目';
      allBtnDesktop.addEventListener('click', () => filterQuestions('all'));
      desktopNav.appendChild(allBtnDesktop);

      const allBtnMobile = document.createElement('button');
      allBtnMobile.id = 'btn-all-mobile';
      allBtnMobile.className = 'category-btn-mobile px-4 py-2 rounded-lg text-left text-sm font-medium transition-all hover:bg-primary/10 hover:text-primary';
      allBtnMobile.textContent = '全部题目';
      allBtnMobile.addEventListener('click', () => {
        filterQuestions('all');
        toggleMobileMenu();
      });
      mobileNav.appendChild(allBtnMobile);

      // 为每个分类生成导航按钮
      total_questions.forEach((category, index) => {
        const categoryName = category.tag;
        // 从标签中提取简短名称用于标识
        const shortName = categoryName.split(' ')[0].toLowerCase();

        // 桌面端按钮
        const btnDesktop = document.createElement('button');
        btnDesktop.id = `btn-${shortName}`;
        btnDesktop.className = 'category-btn px-4 py-2 rounded-full text-sm font-medium transition-all hover:bg-primary/10 hover:text-primary';
        btnDesktop.textContent = categoryName;
        btnDesktop.addEventListener('click', () => filterQuestions(shortName));
        desktopNav.appendChild(btnDesktop);

        // 移动端按钮
        const btnMobile = document.createElement('button');
        btnMobile.id = `btn-${shortName}-mobile`;
        btnMobile.className = 'category-btn-mobile px-4 py-2 rounded-lg text-left text-sm font-medium transition-all hover:bg-primary/10 hover:text-primary';
        btnMobile.textContent = categoryName;
        btnMobile.addEventListener('click', () => {
          filterQuestions(shortName);
          toggleMobileMenu();
        });
        mobileNav.appendChild(btnMobile);
      });
    }

    // 工具函数：生成随机整数
    function getRandomInt(min, max) {
      return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    // 生成所有题目的扁平化列表
    function flattenQuestions() {
      const flatQuestions = [];
      total_questions.forEach((category, categoryIndex) => {
        category.data.forEach((question, questionIndex) => {
          flatQuestions.push({
            ...question,
            category: category.tag,
            categoryIndex,
            questionIndex
          });
        });
      });
      return flatQuestions;
    }

    const flatQuestions = flattenQuestions();
    let currentQuestion = null;
    let currentCategory = 'all';

    // DOM 元素
    const randomBtn = document.getElementById('random-btn');
    const showAnswerBtn = document.getElementById('show-answer-btn');
    const answerContainer = document.getElementById('answer-container');
    const questionCard = document.getElementById('question-card');
    const questionTitle = document.getElementById('question-title');
    const questionContent = document.getElementById('question-content');
    const questionTag = document.getElementById('question-tag');
    const questionCount = document.getElementById('question-count');
    const answers = document.getElementById('answers');
    const questionsList = document.getElementById('questions-list');
    const mobileMenuBtn = document.getElementById('mobile-menu-btn');
    const mobileMenu = document.getElementById('mobile-menu');
    const navbar = document.getElementById('navbar');
    const fixedHideAnswerBtn = document.getElementById('fixed-hide-answer-btn');
    const answerButtonsContainer = document.getElementById('answer-buttons-container');

    // 初始化页面
    function init() {
      // 生成导航栏
      generateNav();

      // 生成题目列表
      renderQuestionsList();

      // 添加事件监听
      randomBtn.addEventListener('click', showRandomQuestion);
      showAnswerBtn.addEventListener('click', toggleAnswer);
      mobileMenuBtn.addEventListener('click', toggleMobileMenu);

      // 固定隐藏答案按钮事件
      fixedHideAnswerBtn.addEventListener('click', toggleAnswer);

      // 滚动效果
      window.addEventListener('scroll', handleScroll);

      // 初始显示一个随机问题
      setTimeout(showRandomQuestion, 500);
    }

    // 显示随机问题
    function showRandomQuestion() {
      // 根据当前分类过滤问题
      let filteredQuestions = flatQuestions;
      if (currentCategory !== 'all') {
        const categoryIndex = total_questions.findIndex(cat =>
          cat.tag.split(' ')[0].toLowerCase() === currentCategory
        );
        if (categoryIndex !== -1) {
          filteredQuestions = flatQuestions.filter(q => q.categoryIndex === categoryIndex);
        }
      }

      if (filteredQuestions.length === 0) return;

      // 随机选择一个问题
      const randomIndex = getRandomInt(0, filteredQuestions.length - 1);
      currentQuestion = filteredQuestions[randomIndex];

      // 显示问题
      questionTitle.textContent = currentQuestion.question;
      // 修改这里，显示 description 而不是 question
      questionContent.textContent = currentQuestion.description || currentQuestion.question;
      questionTag.textContent = currentQuestion.category;
      questionCount.textContent = `第 ${currentQuestion.categoryIndex * 3 + currentQuestion.questionIndex + 1} 题 / 共 ${flatQuestions.length} 题`;

      // 重置答案区域
      answerContainer.classList.add('hidden');
      answers.innerHTML = '';

      // 更新按钮状态
      updateAnswerButtonState();

      // 显示卡片并添加动画
      questionCard.classList.remove('opacity-0', 'scale-95');
      questionCard.classList.add('opacity-100', 'scale-100');

      // 滚动到问题卡片
      questionCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }

    // 切换答案显示状态
    function toggleAnswer() {
      if (answerContainer.classList.contains('hidden')) {
        // 显示答案
        answerContainer.classList.remove('hidden');

        // 渲染答案
        renderAnswers();

        // 添加复制按钮
        addCopyButtons();

        // 应用代码高亮
        applyCodeHighlighting();

        // 更新按钮状态
        updateAnswerButtonState();

        // 滚动到答案区域
        answers.scrollIntoView({ behavior: 'smooth', block: 'start' });
      } else {
        // 隐藏答案
        answerContainer.classList.add('hidden');

        // 更新按钮状态
        updateAnswerButtonState();
      }
    }

    // 更新答案按钮状态
    function updateAnswerButtonState() {
      if (answerContainer.classList.contains('hidden')) {
        // 隐藏状态，显示"查看答案"按钮
        showAnswerBtn.innerHTML = '<i class="fa fa-eye mr-2"></i> 查看参考答案';
        fixedHideAnswerBtn.classList.remove('visible');
      } else {
        // 显示状态，显示"隐藏答案"按钮
        showAnswerBtn.innerHTML = '<i class="fa fa-eye-slash mr-2"></i> 隐藏参考答案';
        fixedHideAnswerBtn.classList.add('visible');
      }
    }

    // 处理滚动事件
    function handleScroll() {
      if (!answerContainer.classList.contains('hidden')) {
        // 只有在答案显示时才处理
        const answerContainerRect = answerContainer.getBoundingClientRect();

        // 如果答案容器的底部超出视口，显示固定按钮
        if (answerContainerRect.bottom > window.innerHeight) {
          fixedHideAnswerBtn.classList.add('visible');
        } else {
          fixedHideAnswerBtn.classList.remove('visible');
        }
      } else {
        // 答案隐藏时，隐藏固定按钮
        fixedHideAnswerBtn.classList.remove('visible');
      }
    }

    // 渲染答案
    function renderAnswers() {
      answers.innerHTML = '';

      currentQuestion.answer.forEach((item, index) => {
        const answerItem = document.createElement('div');
        answerItem.className = 'answer-item bg-gray-50 p-4 rounded-lg border border-gray-200';

        if (item.text) {
          const textElement = document.createElement('p');
          textElement.className = 'mb-3 text-gray-700';
          textElement.innerHTML = `<i class="fa fa-info-circle text-blue-500 mr-2"></i> ${item.text}`;
          answerItem.appendChild(textElement);
        }

        if (item.daima) {
          const codeBlock = document.createElement('div');
          codeBlock.className = 'code-block';

          const codeHeader = document.createElement('div');
          codeHeader.className = 'code-header';
          codeHeader.innerHTML = `
            <span>代码示例 ${index + 1}</span>
            <span class="copy-btn">复制代码</span>
          `;

          const codeBody = document.createElement('pre');
          codeBody.className = 'code-body language-javascript';
          codeBody.textContent = item.daima.trim();

          codeBlock.appendChild(codeHeader);
          codeBlock.appendChild(codeBody);
          answerItem.appendChild(codeBlock);
        }

        answers.appendChild(answerItem);
      });
    }

    // 添加复制按钮功能
    function addCopyButtons() {
      const copyButtons = document.querySelectorAll('.copy-btn');

      copyButtons.forEach(button => {
        button.addEventListener('click', () => {
          const codeBody = button.closest('.code-header').nextElementSibling;
          const textToCopy = codeBody.textContent;

          navigator.clipboard.writeText(textToCopy).then(() => {
            button.textContent = '已复制';
            button.classList.add('copied');

            setTimeout(() => {
              button.textContent = '复制代码';
              button.classList.remove('copied');
            }, 2000);
          }).catch(err => {
            console.error('复制失败: ', err);
            button.textContent = '复制失败';
            setTimeout(() => {
              button.textContent = '复制代码';
            }, 2000);
          });
        });
      });
    }

    // 应用代码高亮
    function applyCodeHighlighting() {
      document.querySelectorAll('pre.code-body').forEach((block) => {
        hljs.highlightElement(block);
      });
    }

    // 渲染题目列表
    function renderQuestionsList() {
      questionsList.innerHTML = '';

      // 根据当前分类过滤问题
      let filteredQuestions = flatQuestions;
      if (currentCategory !== 'all') {
        const categoryIndex = total_questions.findIndex(cat =>
          cat.tag.split(' ')[0].toLowerCase() === currentCategory
        );
        if (categoryIndex !== -1) {
          filteredQuestions = flatQuestions.filter(q => q.categoryIndex === categoryIndex);
        }
      }

      if (filteredQuestions.length === 0) {
        questionsList.innerHTML = `
          <div class="col-span-full text-center py-12">
            <i class="fa fa-search text-gray-400 text-4xl mb-4"></i>
            <p class="text-gray-500">没有找到匹配的题目</p>
          </div>
        `;
        return;
      }

      filteredQuestions.forEach((question, index) => {
        const questionCard = document.createElement('div');
        questionCard.className = 'bg-white rounded-xl shadow-md overflow-hidden card-hover';

        // 设置卡片颜色
        let categoryColor = 'bg-primary/10 text-primary';
        if (question.categoryIndex === 1) categoryColor = 'bg-secondary/10 text-secondary';
        if (question.categoryIndex === 2) categoryColor = 'bg-accent/10 text-accent';

        questionCard.innerHTML = `
          <div class="p-4">
            <span class="px-3 py-1 rounded-full text-xs font-medium ${categoryColor}">
              ${question.category}
            </span>
            <h3 class="mt-3 text-lg font-semibold text-dark line-clamp-2">${question.question}</h3>
            <div class="mt-4 flex justify-between items-center">
              <span class="text-xs text-gray-500">第 ${question.categoryIndex * 3 + question.questionIndex + 1} 题</span>
              <button class="view-question-btn text-primary text-sm font-medium hover:text-primary/80 transition-colors" 
                      data-index="${index}">
                查看题目 <i class="fa fa-arrow-right ml-1"></i>
              </button>
            </div>
          </div>
        `;

        // 添加点击事件
        questionCard.querySelector('.view-question-btn').addEventListener('click', () => {
          currentQuestion = question;
          showSelectedQuestion();
        });

        questionsList.appendChild(questionCard);
      });
    }

    // 显示选中的问题
    function showSelectedQuestion() {
      // 显示问题
      questionTitle.textContent = currentQuestion.question;
      // 修改这里，显示 description 而不是 question
      questionContent.textContent = currentQuestion.description || currentQuestion.question;
      questionTag.textContent = currentQuestion.category;
      questionCount.textContent = `第 ${currentQuestion.categoryIndex * 3 + currentQuestion.questionIndex + 1} 题 / 共 ${flatQuestions.length} 题`;

      // 重置答案区域
      answerContainer.classList.add('hidden');
      answers.innerHTML = '';

      // 更新按钮状态
      updateAnswerButtonState();

      // 显示卡片并添加动画
      questionCard.classList.remove('opacity-0', 'scale-95');
      questionCard.classList.add('opacity-100', 'scale-100');

      // 滚动到问题卡片
      questionCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }

    // 过滤问题
    function filterQuestions(category) {
      currentCategory = category;

      // 更新按钮状态
      const categoryButtons = document.querySelectorAll('.category-btn, .category-btn-mobile');
      categoryButtons.forEach(btn => btn.classList.remove('bg-primary/10', 'text-primary'));

      // 找到当前分类的按钮并激活
      const activeDesktopBtn = document.getElementById(`btn-${category}`);
      const activeMobileBtn = document.getElementById(`btn-${category}-mobile`);

      if (activeDesktopBtn) activeDesktopBtn.classList.add('bg-primary/10', 'text-primary');
      if (activeMobileBtn) activeMobileBtn.classList.add('bg-primary/10', 'text-primary');

      // 如果是"全部"分类，确保对应的按钮也被激活
      if (category === 'all') {
        document.getElementById('btn-all').classList.add('bg-primary/10', 'text-primary');
        document.getElementById('btn-all-mobile').classList.add('bg-primary/10', 'text-primary');
      }

      // 重新渲染题目列表
      renderQuestionsList();

      // 如果当前显示的问题不在过滤结果中，显示一个随机问题
      if (currentCategory !== 'all') {
        const categoryIndex = total_questions.findIndex(cat =>
          cat.tag.split(' ')[0].toLowerCase() === currentCategory
        );

        if (categoryIndex !== -1 &&
          currentQuestion &&
          currentQuestion.categoryIndex !== categoryIndex) {
          showRandomQuestion();
        }
      }
    }

    // 切换移动端菜单
    function toggleMobileMenu() {
      mobileMenu.classList.toggle('hidden');
    }

    // 初始化应用
    init();
  </script>
</body>

</html>